-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 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
-# 长对话「无业务下行」idle:下列 JSON event/type 及裸文本 ping/pong 不重置 close_timerout。
-# 帧级 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 "", ""
-
-
-class AI_led(Abstract):
- def __init__(self):
- try:
- if "vc_config" in globals() and vc_config:
- r_gpio, g_gpio, b_gpio = vc_config.led_gpio_tuple()
- else:
- r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO21, Pin.GPIO19
- except Exception:
- r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO21, Pin.GPIO19
- self._led_gpio_r = r_gpio
- self._led_gpio_g = g_gpio
- self._led_gpio_b = b_gpio
- self.led_red = Pin(r_gpio, Pin.OUT, Pin.PULL_PD, 0)
- self.led_green = Pin(g_gpio, Pin.OUT, Pin.PULL_PD, 1)
- self.led_blue = Pin(b_gpio, Pin.OUT, Pin.PULL_PD, 1)
- print("AI_led GPIO dev_type={} red={} green={} blue={}".format(
- dev_type, r_gpio, g_gpio, b_gpio))
-
- # 统一输入状态(充电/电量/信号/AI对话),按优先级表输出
- self._st_ai = "idle" # idle/listening/thinking/playing
- self._st_charging = "none" # none/charging/full
- self._st_battery = "normal" # normal/low/empty
- self._st_signal = "normal" # normal/weak
- self._st_alert = None # None / rb_flash_2(REQ-ERR-01/03)
-
- self._timer = osTimer()
- self._mode = "off" # off/red_on/green_on/green_blink/blue_blink/red_blink/bg_alt/alert_rb
- self._alert_steps_left = 0
- self._alert_rb_phase = 0
- self._alert_period_ms = 250
- self._blink_on = 0
- self._bg_phase = 0
- self._period_ms = 1000
- self._gb_on_ms = 1000
- self._gb_off_ms = 1000
-
- EventMesh.subscribe("set_led_state", self.set_led_state)
- EventMesh.subscribe("get_led_debug_state", self.get_led_debug_state)
- # LED 电平极性:有些板子是低电平点亮(active-low),也可能仅某一路反相
- self._inv_r = 0
- self._inv_g = 0
- self._inv_b = 0
- try:
- if "vc_config" in globals() and vc_config:
- inv = vc_config.get_attr("LED_ACTIVE_LOW_RGB", None)
- if isinstance(inv, (tuple, list)) and len(inv) == 3:
- self._inv_r = 1 if inv[0] else 0
- self._inv_g = 1 if inv[1] else 0
- self._inv_b = 1 if inv[2] else 0
- else:
- all_inv = 1 if vc_config.get_bool("LED_ACTIVE_LOW", False) else 0
- self._inv_r = all_inv
- self._inv_g = all_inv
- self._inv_b = all_inv
- except Exception:
- pass
- try:
- self._apply_by_table()
- except Exception:
- try:
- self._set_off()
- except Exception:
- pass
-
- def _timer_stop(self):
- try:
- self._timer.stop()
- except Exception:
- pass
-
- def _set_rgb(self, r, g, b):
- try:
- rr = 1 if r else 0
- gg = 1 if g else 0
- bb = 1 if b else 0
- if self._inv_r:
- rr = 0 if rr else 1
- if self._inv_g:
- gg = 0 if gg else 1
- if self._inv_b:
- bb = 0 if bb else 1
- self.led_red.write(rr)
- self.led_green.write(gg)
- self.led_blue.write(bb)
- except Exception:
- pass
-
- def _set_off(self):
- self._mode = "off"
- self._timer_stop()
- self._set_rgb(0, 0, 0)
-
- def _set_red_on(self):
- self._mode = "red_on"
- self._timer_stop()
- self._set_rgb(1, 0, 0)
-
- def _set_green_on(self):
- self._mode = "green_on"
- self._timer_stop()
- self._set_rgb(0, 1, 0)
-
- def _set_green_blink(self, on_ms=1000, off_ms=None):
- """待机/空闲:绿灯慢闪(替代呼吸灯,数字 GPIO 整颗亮灭)。off_ms 缺省则与 on_ms 对称。"""
- on_ms = int(on_ms or 1000)
- if off_ms is None:
- off_ms = on_ms
- else:
- off_ms = int(off_ms or on_ms)
- try:
- if (self._mode == "green_blink" and int(self._gb_on_ms) == int(on_ms)
- and int(self._gb_off_ms) == int(off_ms)):
- return
- except Exception:
- pass
- self._mode = "green_blink"
- self._gb_on_ms = on_ms
- self._gb_off_ms = off_ms
- self._period_ms = on_ms
- self._blink_on = 1
- self._timer_stop()
- self._set_rgb(0, 1, 0)
- self._timer.start(self._gb_on_ms, 0, self._timer_cb)
-
- def _set_blue_blink(self, period_ms=1000):
- self._mode = "blue_blink"
- self._period_ms = period_ms or 1000
- self._blink_on = 1
- self._timer_stop()
- self._set_rgb(0, 0, 1)
- self._timer.start(self._period_ms, 1, self._timer_cb)
-
- def _set_red_blink(self, period_ms=1000):
- self._mode = "red_blink"
- self._period_ms = period_ms or 1000
- self._blink_on = 1
- self._timer_stop()
- self._set_rgb(1, 0, 0)
- self._timer.start(self._period_ms, 1, self._timer_cb)
-
- def _set_bg_alt(self, period_ms=1000):
- self._mode = "bg_alt"
- self._period_ms = period_ms or 1000
- self._bg_phase = 0
- self._timer_stop()
- self._set_rgb(0, 0, 1)
- self._timer.start(self._period_ms, 1, self._timer_cb)
-
- def _set_alert_rb_flash(self, cycles=2, period_ms=250):
- """REQ-ERR-01/03:红蓝快闪(约 2 次/秒),结束后回到 _apply_by_table。"""
- try:
- cycles = int(cycles or 2)
- except Exception:
- cycles = 2
- if cycles < 1:
- cycles = 1
- try:
- period_ms = int(period_ms or 250)
- except Exception:
- period_ms = 250
- if period_ms < 100:
- period_ms = 100
- self._mode = "alert_rb"
- self._st_alert = "rb_flash_2"
- self._alert_period_ms = period_ms
- self._alert_steps_left = cycles * 2
- self._alert_rb_phase = 0
- self._timer_stop()
- self._set_rgb(1, 0, 0)
- self._timer.start(self._alert_period_ms, 0, self._timer_cb)
-
- def _set_red_flash_limited(self, flashes=3, period_ms=500):
- """REQ-BAT-03:红快闪若干次后熄灭(约 2 次/秒)。"""
- try:
- flashes = int(flashes or 3)
- except Exception:
- flashes = 3
- if flashes < 1:
- flashes = 1
- try:
- period_ms = int(period_ms or 500)
- except Exception:
- period_ms = 500
- if period_ms < 100:
- period_ms = 100
- self._mode = "red_flash_n"
- self._red_flash_steps_left = flashes * 2
- self._alert_period_ms = period_ms
- self._blink_on = 1
- self._timer_stop()
- self._set_rgb(1, 0, 0)
- self._timer.start(self._alert_period_ms, 0, self._timer_cb)
-
- def _timer_cb(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo", {"func": self._timer_cb_})
-
- def _timer_cb_(self):
- if self._mode == "green_blink":
- self._blink_on = 0 if self._blink_on else 1
- self._set_rgb(0, 1 if self._blink_on else 0, 0)
- next_ms = self._gb_on_ms if self._blink_on else self._gb_off_ms
- try:
- self._timer.start(next_ms, 0, self._timer_cb)
- except Exception:
- pass
- return
- if self._mode == "blue_blink":
- self._blink_on = 0 if self._blink_on else 1
- self._set_rgb(0, 0, 1 if self._blink_on else 0)
- return
- if self._mode == "red_blink":
- self._blink_on = 0 if self._blink_on else 1
- self._set_rgb(1 if self._blink_on else 0, 0, 0)
- return
- if self._mode == "red_flash_n":
- self._red_flash_steps_left -= 1
- if self._red_flash_steps_left <= 0:
- self._set_off()
- return
- self._blink_on = 0 if self._blink_on else 1
- self._set_rgb(1 if self._blink_on else 0, 0, 0)
- try:
- self._timer.start(self._alert_period_ms, 0, self._timer_cb)
- except Exception:
- pass
- return
- if self._mode == "bg_alt":
- if self._bg_phase == 0:
- self._set_rgb(0, 1, 0)
- self._bg_phase = 1
- else:
- self._set_rgb(0, 0, 1)
- self._bg_phase = 0
- return
- if self._mode == "alert_rb":
- self._alert_steps_left -= 1
- if self._alert_steps_left <= 0:
- self._st_alert = None
- try:
- self._apply_by_table()
- except Exception:
- self._set_off()
- return
- if self._alert_rb_phase == 0:
- self._set_rgb(0, 0, 1)
- self._alert_rb_phase = 1
- else:
- self._set_rgb(1, 0, 0)
- self._alert_rb_phase = 0
- try:
- self._timer.start(self._alert_period_ms, 0, self._timer_cb)
- except Exception:
- pass
- return
-
- def set_led_state(self, event=None, msg=None):
- # msg: dict {ai_state, charging, battery, signal, alert}
- try:
- if isinstance(msg, dict):
- if "alert" in msg:
- aval = msg.get("alert")
- if aval in ("rb_flash_2", "rb_flash", "rb_flash2"):
- self._set_alert_rb_flash(2, 250)
- return
- if aval in ("red_flash_3", "red_flash3"):
- try:
- n = 3
- ms = 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))
- self._set_red_flash_limited(n, ms)
- except Exception:
- self._set_red_flash_limited(3, 500)
- return
- if aval in (None, "", "none", "clear"):
- self._st_alert = None
- if "ai_state" in msg:
- self._st_ai = msg.get("ai_state") or "idle"
- if "charging" in msg:
- self._st_charging = msg.get("charging") or "none"
- if "battery" in msg:
- self._st_battery = msg.get("battery") or "normal"
- if "signal" in msg:
- self._st_signal = msg.get("signal") or "normal"
- except Exception:
- pass
- if self._mode in ("alert_rb", "red_flash_n"):
- return
- if self._led_state_debug_enabled() and isinstance(msg, dict):
- try:
- print("[LED] set_led_state in={}".format(msg))
- except Exception:
- pass
- self._apply_by_table(msg)
-
- def _led_state_debug_enabled(self):
- try:
- if "vc_config" in globals() and vc_config:
- return vc_config.get_bool("LED_STATE_DEBUG", True)
- except Exception:
- pass
- return True
-
- def _led_log(self, tag, extra=""):
- if not self._led_state_debug_enabled():
- return
- try:
- print("[LED] {} ai={} chg={} bat={} sig={} -> mode={} period_ms={} {}".format(
- tag, self._st_ai, self._st_charging, self._st_battery, self._st_signal,
- self._mode, self._period_ms, extra))
- except Exception:
- pass
-
- def _apply_by_table(self, _msg=None):
- if self._mode in ("alert_rb", "red_flash_n"):
- return
- _prev_mode = self._mode
- # P0:低电/空电(高于 AI 交互灯;充电中不显示,走充电灯态)
- try:
- if self._st_charging not in ("charging", "full"):
- if self._st_battery == "empty":
- try:
- ms = 500
- if "vc_config" in globals() and vc_config:
- ms = int(vc_config.get_int("BATTERY_EMPTY_FLASH_PERIOD_MS", 500))
- self._set_red_blink(ms)
- except Exception:
- self._set_red_blink(500)
- return
- if self._st_battery == "low":
- self._set_red_blink(1000)
- return
- except Exception:
- pass
- # P1~P3:AI 阶段(播放 > 思考 > 聆听)
- try:
- if self._st_ai == "playing":
- self._set_green_on()
- return
- if self._st_ai == "thinking":
- # 蓝绿「交替」:约 0.5 s 换一色(`period_ms` 为换色间隔)
- self._set_bg_alt(500)
- return
- if self._st_ai == "listening":
- # 长对话蓝慢闪(≈1s);单对话 PTT 蓝快闪(≈2次/s,REQ-AI-07)
- period_ms = 1000
- try:
- if EventMesh.publish("get_storage_config", "VC_mode") == 1:
- period_ms = 250
- except Exception:
- pass
- self._set_blue_blink(period_ms)
- return
- except Exception:
- pass
- # AI 交互中(聆听/思考/播放/流播)不让充电灯抢占;交互结束 idle 后恢复充电红/充满绿(REQ 冲突规避)
- _skip_charge_led = 0
- try:
- if self._st_ai in ("listening", "thinking", "playing"):
- _skip_charge_led = 1
- except Exception:
- pass
- if not _skip_charge_led:
- try:
- if EventMesh.publish("get_vc_stream_active") == 1:
- _skip_charge_led = 1
- except Exception:
- pass
- # 基础态:充电/电量/信号
- try:
- if not _skip_charge_led:
- if self._st_charging == "full":
- self._set_green_on()
- return
- if self._st_charging == "charging":
- self._set_red_on()
- return
- if self._st_signal == "weak":
- # 弱网:蓝绿「慢交替」,与 thinking 同节拍定义(约 1 s 换一色)
- self._set_bg_alt(1000)
- return
- except Exception:
- pass
- # 空闲待机:默认灭灯(REQ-BAT-01 / GAP-005 方案甲);LED_IDLE_MODE=green_blink 可恢复旧工程绿灯慢闪
- try:
- idle_mode = "off"
- if "vc_config" in globals() and vc_config:
- _im = vc_config.get_attr("LED_IDLE_MODE", None)
- if _im is not None:
- idle_mode = str(_im).strip().lower()
- if idle_mode in ("green_blink", "blink", "green"):
- idle_on = 1000
- if "vc_config" in globals() and vc_config:
- idle_on = int(vc_config.get_int("LED_IDLE_GREEN_BLINK_MS", 1000))
- idle_off = idle_on
- if "vc_config" in globals() and vc_config:
- _ioff = vc_config.get_attr("LED_IDLE_GREEN_BLINK_OFF_MS", None)
- if _ioff is not None:
- try:
- idle_off = int(_ioff)
- except Exception:
- idle_off = idle_on
- if idle_on < 400:
- idle_on = 400
- if idle_off < 400:
- idle_off = 400
- self._set_green_blink(idle_on, idle_off)
- else:
- self._set_off()
- except Exception:
- self._set_off()
- if self._mode != _prev_mode:
- self._led_log("apply_table", "in={}".format(_msg))
-
- def get_led_debug_state(self, event=None, msg=None):
- # 供按键/状态机打印当前 LED 聚合状态与最终模式
- try:
- return {
- "ai": self._st_ai,
- "charging": self._st_charging,
- "battery": self._st_battery,
- "signal": self._st_signal,
- "mode": self._mode,
- "period_ms": self._period_ms,
- }
- except Exception:
- return {"mode": getattr(self, "_mode", "?")}
-
-
-def _default_battery_ocv_map():
- # 与 mgr.py BATTERY_OCV_TABLE nix_coy_mnzo2 默认一致;可被 default_config.BATTERY_OCV_MAP 覆盖
- return {
- 4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,
- 3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,
- }
-
-
-def _parse_battery_ocv_map(raw):
- """解析 default_config.BATTERY_OCV_MAP:dict 或 [(mv, soc), ...]。"""
- if raw is None:
- return None
- out = {}
- try:
- if isinstance(raw, dict):
- pairs = raw.items()
- elif isinstance(raw, (tuple, list)):
- pairs = ((p[0], p[1]) for p in raw if isinstance(p, (tuple, list)) and len(p) >= 2)
- else:
- return None
- for k, v in pairs:
- out[int(k)] = int(v)
- return out if out else None
- except Exception:
- return None
-
-
-def _soc_from_vbatt_mv(volt_mv, ocv_map, ocv_volts, vbatt_simple=None):
- """由电压(mV) 算 SoC(0~100);配置来自 default_config / _BatteryCfg。"""
- try:
- v = int(volt_mv)
- except Exception:
- return None
- try:
- if vbatt_simple:
- _full, _v20, _v5 = vbatt_simple
- if v >= int(_full):
- return 100
- if v <= int(_v5):
- return 0
- if v <= int(_v20):
- return 10
- return 60
- except Exception:
- pass
- try:
- volts = ocv_volts
- if not volts:
- return None
- pre_volt = 0
- volt_not_under = 0
- soc1 = 0
- soc2 = 0
- for vv in volts:
- if v > vv:
- volt_not_under = 1
- soc1 = ocv_map.get(vv, 0)
- soc2 = ocv_map.get(pre_volt, 0)
- break
- else:
- pre_volt = vv
- if pre_volt == 0:
- return soc1
- if volt_not_under == 0:
- return 0
- return int(soc2 - (soc2 - soc1) * (pre_volt - v) // (pre_volt - vv))
- except Exception:
- return None
-
-
-class _BatteryCfg(object):
- """从 vc_config(default_config) 加载电池参数,供 AI_hw_status / get_battery / AI_dev 共用。"""
- _inst = None
-
- def __init__(self):
- self.ocv_map = _default_battery_ocv_map()
- self.ocv_volts = sorted(self.ocv_map.keys(), reverse=True)
- self.charge_full_mv = 4290
- self.low_mv = 3699
- self.empty_mv = 3629
- self.low_soc = 20
- self.empty_soc = 5
- self.vbatt_simple = None
- try:
- if "vc_config" in globals() and vc_config:
- self.charge_full_mv = int(vc_config.get_int("VBATT_CHARGE_FULL_MV", 4290))
- self.low_mv = int(vc_config.get_int("VBATT_LOW_MV", 3699))
- self.empty_mv = int(vc_config.get_int("VBATT_EMPTY_MV", 3629))
- self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 30))
- self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 10))
- _ocv_cfg = _parse_battery_ocv_map(vc_config.get_attr("BATTERY_OCV_MAP", None))
- if _ocv_cfg:
- self.ocv_map = _ocv_cfg
- self.ocv_volts = sorted(self.ocv_map.keys(), reverse=True)
- if vc_config.get_bool("USE_SIMPLE_VBATT_THRESH", False):
- self.vbatt_simple = (self.charge_full_mv, self.low_mv, self.empty_mv)
- except Exception:
- pass
-
- @classmethod
- def instance(cls):
- if cls._inst is None:
- cls._inst = cls()
- return cls._inst
-
- def soc_from_vbatt(self, volt_mv):
- return _soc_from_vbatt_mv(volt_mv, self.ocv_map, self.ocv_volts, self.vbatt_simple)
-
-
-class AI_hw_status(Abstract):
- # 精简硬件状态采集:仅维护 LED 状态机所需的 charging/battery/signal
- def __init__(self):
- # 无条件首行:确认是否进入初始化(避免被异常/注册顺序误判)
- try:
- print("[HW] __init__ enter")
- except Exception:
- pass
- _bcfg = _BatteryCfg.instance()
- self._bcfg = _bcfg
- self._ocv_map = _bcfg.ocv_map
- self._ocv_volts = _bcfg.ocv_volts
- self._charge_full_mv = _bcfg.charge_full_mv
- self._low_soc = _bcfg.low_soc
- self._empty_soc = _bcfg.empty_soc
- self._vbatt_simple = _bcfg.vbatt_simple
- # cached state (avoid repeat publish)
- self._chg = None
- self._bat = None
- self._sig = None
- self._usb = None
- self._charge_full_latched = 0
- self._charge_full_streak = 0
- self._charge_full_streak_need = 2
- self._timer = osTimer()
- self._period_ms = 10000
- self._weak_csq = 10
- self._dbg = 0
- try:
- if "vc_config" in globals() and vc_config:
- self._period_ms = vc_config.get_int("HW_STATUS_PERIOD_MS", 10000) or 10000
- self._weak_csq = vc_config.get_int("SIGNAL_WEAK_CSQ", 10) or 10
- self._dbg = 1 if vc_config.get_bool("HW_STATUS_DEBUG", False) else 0
- except Exception:
- pass
- # 若开启了 LED/按键调试,也一并打开硬件监控打印,便于关联分析
- try:
- if "vc_config" in globals() and vc_config:
- if vc_config.get_bool("LED_DEBUG", False) or vc_config.get_bool("KEY_LED_DEBUG", False):
- self._dbg = 1
- except Exception:
- pass
-
- # 注意:AI_dev 也会注册 USB callback;USB 模块通常只支持一个 callback,后注册会覆盖先注册。
- # 为避免“刚开始有效,后面失效”,这里不直接 setCallback,改为订阅 AI_dev 转发的 USB 状态事件,
- # 并在 10 秒轮询中再读一次 GET_USB_STATE 作兜底。
- try:
- EventMesh.subscribe("hw_usb_state", self._on_usb_state)
- except Exception:
- pass
-
- # 初始化:先立即刷新一次,再开启周期轮询(避免 one-shot/覆盖差异导致只跑一次)
- try:
- self._update()
- except Exception:
- pass
- try:
- self._timer.start(self._period_ms, 1, self._tick_cb)
- except Exception:
- pass
- # 初始化打印(一次)
- try:
- print("[HW] init ok period_ms={} weak_csq={} usb_init={} algo={}".format(
- self._period_ms,
- self._weak_csq,
- self._usb,
- "simple" if getattr(self, "_vbatt_simple", None) else "ocv"))
- if self._dbg == 1:
- print("[HW] init detail vbatt_simple={}".format(getattr(self, "_vbatt_simple", None)))
- except Exception:
- pass
-
- def _on_usb_state(self, event=None, msg=None):
- try:
- state = msg
- prev = self._usb
- self._usb = 1 if state == 1 else 0
- if self._usb == 1 and prev != 1:
- self._charge_full_latched = 0
- self._charge_full_streak = 0
- if self._usb == 0:
- self._charge_full_latched = 0
- self._charge_full_streak = 0
- except Exception:
- pass
- try:
- if self._dbg == 1:
- print("[HW] usb_evt state={} usb={}".format(state, self._usb))
- except Exception:
- pass
- try:
- self._update()
- except Exception:
- pass
-
- def _tick_cb(self, *args, **kwargs):
- # 定时器回调直接刷新(不依赖 AI_dev 的事件线程)
- try:
- self._tick()
- except Exception:
- pass
-
- def _tick(self, *args, **kwargs):
- self._update()
-
- def _update(self):
- # 每次刷新都“同步推送一次”当前硬件基础态到 LED(不依赖变化检测)
- # 目的:保证 10 秒轮询和 USB callback 都能及时刷新充电/电量/信号的基础态显示
- # charging/battery
- try:
- v = Power.getVbatt()
- except Exception:
- v = None
- soc = None
- csq = None
- rssi = None
- usb_status = None
- chg = "none"
- bat = "normal"
- sig = "normal"
- try:
- usb_status = self._usb
- if usb_status is None:
- try:
- usb_status = EventMesh.publish("GET_USB_STATE")
- if usb_status is None:
- usb_status = 0
- except Exception:
- usb_status = 0
- # charging:插入后先红灯;勿用瞬时 vbatt≥满电阈值直接判满(充电器接入电压即偏高)
- if usb_status == 1:
- if getattr(self, "_charge_full_latched", 0) == 1:
- chg = "full"
- else:
- if v is not None and v >= int(self._charge_full_mv):
- self._charge_full_streak = int(getattr(self, "_charge_full_streak", 0)) + 1
- need = int(getattr(self, "_charge_full_streak_need", 2) or 2)
- if self._charge_full_streak >= need:
- self._charge_full_latched = 1
- chg = "full"
- else:
- chg = "charging"
- else:
- self._charge_full_streak = 0
- chg = "charging"
- # battery level for LED table
- if usb_status != 1 and v is not None:
- soc = self._bcfg.soc_from_vbatt(v)
- if soc is not None:
- if soc < int(self._empty_soc):
- bat = "empty"
- elif soc < int(self._low_soc):
- bat = "low"
- self._chg = chg
- self._bat = bat
- except Exception:
- pass
-
- # signal
- try:
- csq = net.csqQueryPoll()
- # csqQueryPoll 返回 (rssi, ber) 或 int,按保守取 rssi
- try:
- if isinstance(csq, (tuple, list)) and len(csq) > 0:
- rssi = csq[0]
- else:
- rssi = csq
- except Exception:
- rssi = csq
- sig = "weak" if (rssi is not None and int(rssi) >= 0 and int(rssi) < int(self._weak_csq)) else "normal"
- self._sig = sig
- except Exception:
- pass
-
- # 统一推送一次当前基础态(一次调用带齐字段,减少来回切换)
- try:
- EventMesh.publish("set_led_state", {"charging": chg, "battery": bat, "signal": sig})
- except Exception:
- pass
-
- # 轮询调试打印(每次 10s tick 一次):硬件采样一行 + LED 聚合状态一行内打完
- try:
- if self._dbg == 1:
- led = None
- try:
- led = EventMesh.publish("get_led_debug_state")
- except Exception:
- pass
- print("[HW] usb={} vbatt_mv={} soc={} chg={} bat={} csq={} rssi={} sig={} period_ms={} led={}".format(
- usb_status, v, soc, self._chg, self._bat, csq, rssi, self._sig, self._period_ms, led))
- except Exception:
- pass
-
- def _soc_from_vbatt(self, volt_mv):
- return self._bcfg.soc_from_vbatt(volt_mv)
-
-
-def set_ipv4_datacall():
- # 获取第一路的APN,ipType 等信息
- pdpCtx = dataCall.getPDPContext(1)
- if pdpCtx != -1:
- if pdpCtx[0] != 0:
- # 如果不是用户需要的ipv4,使用如下方式配置,使用如下方式配置
- ret = dataCall.setPDPContext(1, 0,pdpCtx[1],pdpCtx[2],pdpCtx[3],pdpCtx[4])
- if ret == 0:
- print('配置成功。')
- # cfun0/1后按照配置的信息进行拨号
- net.setModemFun(0)
- net.setModemFun(1)
- else:
- print('配置失败。')
- else:
- print('已经配置为IPv4')
- else:
- print('获取PDP Context失败。')
-
-#HEARDER+TOTALLEN+DATELEN+DATA
-HEARDER = "FFFE"
-TOTALLEN = "00000000"
-DATELEN = "0000"
-DATA = {}
-class AI_UART():
- #串口命令通道
- def __init__(self):
- self.send_count=0
- self.AI_uart = UART(UART.UART4, 230400, 8, 0, 1, 0)
- self.AI_uart.set_callback(self.uart_call)
-
- EventMesh.subscribe("bt_ota_run",self.bt_ota_run)#
- EventMesh.subscribe("start_ota",self.start_ota)
- EventMesh.subscribe("end_ota",self.end_ota)
- EventMesh.subscribe("send_bt_switch_toble",self.send_bt_switch_toble)
- EventMesh.subscribe("send_ble_switch_toble",self.send_ble_switch_toble)
- EventMesh.subscribe("send_ble_scan",self.send_ble_scan)
- EventMesh.subscribe("send_connect_bt_mac",self.send_connect_bt_mac)
- EventMesh.subscribe("send_disconnect_bt_mac",self.send_disconnect_bt_mac)
- EventMesh.subscribe("send_imei_to_ble",self.send_imei_to_ble)
- EventMesh.subscribe("send_get_connect_bt_state",self.send_get_connect_bt_state)
- EventMesh.subscribe("recv_bt_connect_rest",self.recv_bt_connect_rest)
- EventMesh.subscribe("AI_send_bt_ble_switch_toble",self.AI_send_bt_ble_switch_toble)
- EventMesh.subscribe("get_bt_versions",self.get_bt_versions)
- EventMesh.subscribe("adjust_volume_toBle",self.adjust_volume_toBle)
- EventMesh.subscribe("clear_earphone_record",self.clear_earphone_record)
- self.bt_switch = 0
- self.ble_switch = 0
- self.uart_timer = osTimer()
- self.get_bt_connect_timer = osTimer()
- self.get_bt_connect_flag = 0
- self.uart_timer_count = 0
- self.bt_ota_flag = 0
- self.read_ack_flag = 0
- self.ping_json_str = {}
- self.ping_json_str_tmp =""
- self.read_uart_data = ""
- self.bt_versions=""
- try:
- uart_ok = False
- if "vc_config" in globals() and vc_config:
- uart_ok = vc_config.uart_supported()
- except Exception:
- uart_ok = False
- if uart_ok:
- self.uart_send_queue = Queue(100)
- self.uart_send_queue_thread_id = _thread.start_new_thread(self.uart_send_queue_thread, ())
-
- self.uart_recv_queue = Queue(100)
- self.uart_recv_queue_thread_id = _thread.start_new_thread(self.uart_recv_queue_thread, ())
- self.send_get_versions()#获取蓝牙版本
-
- def uart_send_queue_thread(self):
- while True:
- data = self.uart_send_queue.get()
-
- #EventMesh.debug_log("Info_queue:data = {}".format(data))
- if data != None:
- try:
- if len(data) != 0:
- print("send_queu_uart_data2:{} data:{}".format(data,data["data"]))
- # json_str =ujson.dumps(data)
- if data["event"] == "uart_send_data":
- print("send_queu_uart_data sucess data:{} {}".format(data["data"],type(data["data"])))
- self.uart_queue_send_data(data["data"])
- if "resend_flag" in data:
- print("send_reset_cb:{} {}".format(data["resend_flag"],data["ACK_time"]))
- if data["resend_flag"] == 1:
- self.send_count = 5#重试5次
- ack_timer = 0 #单次最大等待时间
- count = 0
- if "ACK_time" in data:
- if data["ACK_time"] == None:
- ack_timer = 100#最大重发5次,没ack或者返回error重发
- else:
- ack_timer = data["ACK_time"]
- if ack_timer > 0:
- while self.send_count:
-
- if self.send_count <=0 or self.read_ack_flag == 1:
- print("4G串口重发机制退出:{} self.read_ack_flag:{} json_str:{}".format(self.send_count,self.read_ack_flag,data["data"]))
- break
- if count >= ack_timer:
- count = 0
- print("串口异常,重发:")
- self.uart_queue_send_data(data["data"])
- self.send_count-=1
- count += 1
- utime.sleep_ms(1)
-
- except Exception as e:
- usys.print_exception(e)
- utime.sleep_ms(10)
-
- def uart_recv_queue_thread(self):
- while True:
- data = self.uart_recv_queue.get()
-
- #EventMesh.debug_log("Info_queue:data = {}".format(data))
- if data != None:
- try:
- if len(data) != 0:
- print("recv_queu_uart_data2:{}".format(data))
- # json_str =ujson.dumps(data)
- # queue_data={"event":"uart_recv_data"}
- if data["event"] == "uart_recv_data":
- print("recv_queu_uart_data sucess data:{}".format(type(data["event"])))
- self.pares_uart_data()
- except Exception as e:
- usys.print_exception(e)
-
- def send_imei_to_ble(self,event,msg):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.send_imei_to_ble_cb})
-
- def send_imei_to_ble_cb(self):
- self.read_ack_flag=0
- imei = modem.getDevImei()
- json_data = \
- {
-
- "event": "imei",
- "data":str(imei)
-
- }
- json_str =ujson.dumps(json_data)
- print("send imei:{}".format(json_str))
- self.send_data(json_str)
- pass
- def send_connect_bt_mac(self,event,msg):
- self.read_ack_flag=0
- json_data = \
- {
- "event": "connect_bt",
- "data":msg
- }
- json_str =ujson.dumps(json_data)
- self.send_data(json_str,1,2000)
- # self.send_reset_cb(json_str,0,2000) #每2s发送一次 10ms为单位
- pass
- def send_disconnect_bt_mac(self,event,msg):
- self.read_ack_flag=0
- json_data = \
- {
- "event": "disconnect_bt",
- "data":msg
- }
- json_str =ujson.dumps(json_data)
- self.send_data(json_str,1)
- # self.send_reset_cb(json_str)
-
- pass
- def uart_timer_cb(self, *args, **kwargs):
- self.uart_timer_count+=1
- if self.uart_timer_count >40:
- self.read_uart_data = ""
- if self.uart_timer_count >200:
- self.read_uart_data = ""
- self.uart_timer.stop()
- def split_combined_json(self,combined_str):
- # 将连续的JSON字符串分割为多个独立的JSON字符串列表
- #
- # 参数:
- # combined_str: 多个JSON拼接的字符串
- # 返回:
- # 分割后的
- json_list = []
- start_index = 0
- brace_count = 0 # 用于匹配{}括号
- print("combined_str:{}".format(combined_str))
- for i, char in enumerate(combined_str):
- if char == '{':
- brace_count += 1
- elif char == '}':
- brace_count -= 1
-
- # 当括号完全匹配(count=0)时,说明一个完整的JSON对象结束
- if brace_count == 0:
- # 提取从start_index到当前位置的子串(包含当前})
- json_str = combined_str[start_index:i+1].strip()
- if json_str: # 避免空字符串
- json_list.append(json_str)
- start_index = i + 1 # 下一个JSON的起始位置
- print("json_list{}".format(json_list))
-
- return json_list
-
- def uart_call(self,para):
- queue_data={"event":"uart_recv_data"}
-
- self.uart_recv_queue.put(queue_data)
- # print("uart_call start")
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.pares_uart_data})
- # print("uart_call end")
- #print("ai_uart:{}".format(para))
- #[{"event":""},{},{}]
- def pares_uart_data(self):
- uart_data = self.AI_uart.read()
- self.uart_timer.start(10, 1, self.uart_timer_cb)
- self.uart_timer_count = 0
- ack_flag = 0#是否要返回ack
- print("uart_data:{}",uart_data)
- uart_data_str = uart_data.decode('utf-8')
-
- self.read_uart_data = self.read_uart_data +uart_data_str
- try:
- ujson.loads(self.read_uart_data)
- except Exception as e:
- print("json数据不完整:self.read_uart_data{}".format(self.read_uart_data))
- usys.print_exception(e)
- return
- self.uart_timer.stop()
- self.uart_timer_count = 0
- self.playload_data(self.read_uart_data)
-
- # 分割JSON
- # json_strings = self.split_combined_json(uart_data_str)
-
- # # 逐个解析并处理
- # for i, json_str in enumerate(json_strings, 1):
- # print("第{}个JSON{}".format(i,json_str))
- # try:
- # data = ujson.loads(json_str)
-
- # # 在这里添加你的业务逻辑,比如根据event处理不同事件
- # self.uart_timer.stop()
- # self.uart_timer_count = 0
- # self.playload_data(self.read_uart_data)
- # # if data.get("event") == "bt_connect_single":
-
- # # elif data.get("event") == "bt_scan_rest":
-
- # except Exception as e:
- # print("JSON解析错误:原始字符串:")
- # print("-" * 50)
-
-
-
- # print("uart_data_str:{}",uart_data_str)
- # if len(uart_data_str) < (HEARDER+1+1):
- # return
- # head = uart_data_str[0:4]
- # pack_flag = uart_data_str[4:5]
- # recvflag = uart_data_str[5:6]
-
-
- # if head == HEARDER:
- # if pack_flag == "1": #分包数据
- # if recvflag == "1": #未发送完成
- # self.ping_json_str_tmp+=uart_data_str[6:]
- # pass
- # else:
- # self.playload_data(self.ping_json_str_tmp)
- # pass
- # else:
- # self.playload_data(uart_data_str[6:])
-
- # else:
- # print("uart is not FFFE:")
-
- def playload_data(self,msg):
- ack_flag=0
- try:
- print("playload_data:{}".format(msg))
- jsload_data = ujson.loads(msg)
- #print("AI_client_VC_recv jsload_data = {}".format(jsload_data))
-
- if jsload_data["event"] == "AI_KEY_PRESS" :#AI按键按下
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press})
- ack_flag = 1
- elif jsload_data["event"] == "BT_AI_KEY_PRESS" :#蓝牙耳机key ai对话模式不支持长按对话模式 只支持短按按键对话模式 重新定义按键
- EventMesh.publish("publish_AI_btnInfo",{"func":self.BT_AI_key_press})
- ack_flag = 1
- elif jsload_data["event"] == "AI_PWR_PRESS" :#电源按键按下
- EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_release})
- ack_flag = 1
- elif jsload_data["event"] == "bt_scan_rest":#扫描列表 #蓝牙停止扫描后主动向4g发送扫描列表需要发送ack
- EventMesh.publish("send_bt_scan_list",jsload_data)
- ack_flag=1
- elif jsload_data["event"]=="bt_connect_single": #收到蓝牙主动发送过来的单个连接蓝牙耳机的连接状态 需要发送ack
- EventMesh.publish("save_bt_connect_list",jsload_data)
- ack_flag=1
-
- elif jsload_data["event"]=="need_send_bt_connect_list": #发送连接蓝牙连接记录列表 不需要发送ack
- self.get_bt_connect_timer.stop()
- self.get_bt_connect_flag = 0
- EventMesh.publish("save_bt_connect_list",jsload_data) #先保存连接记录再发送连接耳机列表to IOT
- EventMesh.publish("send_bt_connect_list")
-
- elif jsload_data["event"]=="get_ble_switch":
- ble_switch_flag = EventMesh.publish("get_storage_config","switch_ble_state")#ble开关 不需要发送ack
- if ble_switch_flag == None:
- ble_switch_flag = "1"
- EventMesh.publish("send_ble_switch_toble",str(ble_switch_flag))
-
- pass
- elif jsload_data["event"]=="get_bt_switch":
- bt_switch_flag = EventMesh.publish("get_storage_config","switch_bt_state")#bt开关 不需要发送ack
- if bt_switch_flag == None:
- bt_switch_flag = "1"
- EventMesh.publish("send_bt_switch_toble",bt_switch_flag)
-
- pass
- elif jsload_data["event"]=="get_cat1_imei": #蓝牙主动向4G获取imei 4g不需要发送ack 蓝牙收到imei需要回ack给4改
- EventMesh.publish("send_imei_to_ble")
- ack_flag=1
- pass
- elif jsload_data["event"] == "post_bt_connect_rest": #蓝牙主动蓝牙耳机连接状态 需要发送ack给蓝牙
- EventMesh.publish("recv_bt_connect_rest",jsload_data)
- ack_flag=1
-
- elif jsload_data["event"] == "clear_earphone_record_ok":#清除蓝牙耳机连接记录
- tts_play_str = "删除成功"
- EventMesh.publish("play_tts",[tts_play_str,1])
- self.read_ack_flag = 1
-
- elif jsload_data["event"] == "versions":
- self.bt_versions = jsload_data["data"]
- print("当前蓝牙版本是:{}".format(self.bt_versions))
- self.read_ack_flag = 1
-
- # elif jsload_data["event"] == "kws" :#
- # EventMesh.publish("kws_even")
- elif jsload_data["event"] == "ack" :#
- self.read_ack_flag = 1
- if ack_flag == 1:
- self.send_ack()
- self.read_uart_data = ""
- except Exception as e:
- usys.print_exception(e)
- self.read_uart_data = ""
- def recv_bt_connect_rest(self,event,msg):
- date_tmp = utime.localtime()
- time_tmp = utime.mktime(date_tmp)
- if "data" in msg:
- EventMesh.publish("send_bt_connect_state_to_iot",msg)
- pass
- def get_bt_versions(self,event=None,msg=None):
- return self.bt_versions
- def clear_earphone_record(self,event,msg): #清楚所有耳机记录
-
- json_data = \
- {
- "event": "clear_earphone_record",
- }
- json_str =ujson.dumps(json_data)
- print("clear_earphone_record{}".format(json_str))
- self.send_data(json_str,1)
- def adjust_volume_toBle(self,event,msg): #
- self.read_ack_flag=0
- volume = int(msg)
- if volume <= 10:
- volume = 10
- json_data = \
- {
- "event": "adjust_volume",
- "data":str(volume)
- }
- json_str =ujson.dumps(json_data)
- print("adjust_volume_toBle{}".format(json_str))
- self.send_data(json_str,1)
-
- def AI_send_bt_ble_switch_toble(self,event,msg):
- # // {
- # // "event":"switch_bt_state",
- # // "data":{
- # // "switch_bt_state":"0"
- # // }
- # // } #格式
-
- self.read_ack_flag=0
- if msg == 1: #AI对话开启蓝牙
- json_data = \
- {
-
- "event": "switch_ble_state",
- "data":{
- "switch_ble_state":"1"
- }
-
- }
- json_str =ujson.dumps(json_data)
- print("switch_ble_state{}".format(json_str))
- self.send_data(json_str,1)
-
- elif msg == 2: #AI对话开启蓝牙
- json_data = \
- {
-
- "event": "switch_ble_state",
- "data":{
- "switch_ble_state":"0"
- }
-
- }
- json_str =ujson.dumps(json_data)
- print("switch_ble_state{}".format(json_str))
- self.send_data(json_str,1)
- elif msg == 3: #AI对话开启蓝牙
- json_data = \
- {
-
- "event": "switch_bt_state",
- "data":{
- "switch_bt_state":"1"
- }
- }
- json_str =ujson.dumps(json_data)
- print("switch_bt_state{}".format(json_str))
- self.send_data(json_str,1)
-
- elif msg == 4: #AI对话开启蓝牙
- json_data = \
- {
-
- "event": "switch_bt_state",
- "data":{
- "switch_bt_state":"0"
- }
-
- }
- json_str =ujson.dumps(json_data)
- print("switch_bt_state{}".format(json_str))
- self.send_data(json_str,1)
-
- def send_bt_switch_toble(self,event,msg):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.send_bt_switch_toble_cb,"data":msg})
- def send_bt_switch_toble_cb(self,data):
- # // {
- # // "event":"switch_bt_state",
- # // "data":{
- # // "switch_bt_state":"0"
- # // }
- # // } #格式
- self.read_ack_flag=0
- json_data = \
- {
-
- "event": "switch_bt_state",
- "data":{
- "switch_bt_state":str(data)
- }
-
- }
- json_str =ujson.dumps(json_data)
- print("send_bt_switch_toble{}".format(json_str))
- self.send_data(json_str,1)
-
- def send_ble_switch_toble(self,event,msg):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.send_ble_switch_toble_cb,"data":msg})
- def send_ble_switch_toble_cb(self,data):
- # // {
- # // "event":"switch_ble_state",
- # // "data":{
- # // "switch_ble_state":"0"
- # // }
- # // } #格式
-
- self.read_ack_flag=0
- json_data = \
- {
- "event": "switch_ble_state",
- "data":{
- "switch_ble_state":str(data)
- }
- }
- json_str =ujson.dumps(json_data)
- print("send_ble_switch_toble{}".format(json_str))
- self.send_data(json_str,1)
-
- # 重发机制
- def send_reset_cb(self,json_str,need_resend = None,timeOut=None):
- print("send_reset_cb:{} {} {}".format(json_str,need_resend,timeOut))
- if timeOut == None:
- self.send_count = 100#最大重发5次,没ack或者返回error重发
- else:
- self.send_count = timeOut
-
- while self.send_count:
- self.send_count-=1
- utime.sleep_ms(10)
- if self.send_count <=0 or self.read_ack_flag == 1:
- print("4G串口重发机制退出:{} self.read_ack_flag:{} json_str:{}".format(self.send_count,self.read_ack_flag,json_str))
- break
- if self.send_count%(self.send_count//10) == 0:
- print("串口异常,重发:")
- self.send_data(json_str)
-
- pass
- def send_get_connect_bt_state(self,event,msg):
- json_data = \
- {
-
- "event": "get_connect_bt_state"
- }
- json_str =ujson.dumps(json_data)
- print("send_get_connect_bt_state{}".format(json_str))
- self.send_data(json_str)
- self.get_bt_connect_flag = 1
- self.get_bt_connect_timer.start(2000,0,self.get_bt_connect_timer_cb)
- def get_bt_connect_timer_cb(self,*agrc):
- if self.get_bt_connect_flag == 1:
- EventMesh.publish("send_bt_connect_list")
- pass
- self.get_bt_connect_flag = 0
- pass
- def send_ble_scan(self,event,msg):
- self.read_ack_flag=0
- # // {
- # // "event":"send_ble_scan",
- # // "data":{
- # // "send_ble_scan":"0"
- # // }
- # // } #格式
- json_data = \
- {
-
- "event": "bt_scan",
- "data":msg
-
- }
- json_str =ujson.dumps(json_data)
- print("send_ble_scan{}".format(json_str))
- self.send_data(json_str,1)
- # self.send_reset_cb(json_str)
-
- print("bt_send_end")
- def send_data(self,str_data,need_resend = None,ACK_time =None):
- try:
- if not ("vc_config" in globals() and vc_config and vc_config.uart_supported()):
- return
- except Exception:
- return
- if self.bt_ota_flag == 1:
- print("ota 中不需要上传")
- return
- print("send_data{} {} {}".format(str_data,need_resend,ACK_time))
- queue_data={"event":"uart_send_data","data":str_data}
- if need_resend != None:
- queue_data={"event":"uart_send_data","data":str_data,"resend_flag":1,"ACK_time":ACK_time}
-
- self.uart_send_queue.put(queue_data)
- def uart_queue_send_data(self,str_data):
- if self.bt_ota_flag == 1:
- return
- #EventMesh.publish("wakeup_BT")
- print("bt_seuart_queue_send_data start {}".format(str_data))
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- self.AI_uart.write(str_data)
- print("bt_seuart_queue_send_data end {}".format(str_data))
- def led_r_open(self,event=None,msg=None):
- json_data = \
- {
-
- "event": "led_r_open",
-
-
- }
- json_str =ujson.dumps(json_data)
- print("led_r_open{}".format(json_str))
- self.send_data(json_str)
- def led_g_open(self,event=None,msg=None):
- json_data = \
- {
-
- "event": "led_g_open",
-
- }
- json_str =ujson.dumps(json_data)
- print("led_g_open{}".format(json_str))
- self.send_data(json_str)
- def led_b_open(self,event=None,msg=None):
- json_data = \
- {
-
- "event": "led_b_open",
-
- }
- json_str =ujson.dumps(json_data)
- print("led_b_open{}".format(json_str))
- self.send_data(json_str)
- def led_rgb_open(self,event=None,msg=None):
- json_data = \
- {
-
- "event": "led_rgb_open",
-
- }
- json_str =ujson.dumps(json_data)
- print("led_rgb_open{}".format(json_str))
- self.send_data(json_str)
- def led_close(self,event=None,msg=None):
- json_data = \
- {
-
- "event": "led_close",
-
- }
- json_str =ujson.dumps(json_data)
- print("led_close{}".format(json_str))
- self.send_data(json_str)
-
- def send_ack(self):
- json_data = \
- {
-
- "event": "ack",
-
- }
- json_str =ujson.dumps(json_data)
- print("send_ack{}".format(json_str))
- self.send_data(json_str)
- def AI_key_press(self):
- self.check_rest_timer_count = 0
- print("AI_key_press")
- EventMesh.publish("AI_key_press")
- def BT_AI_key_press(self):
- self.check_rest_timer_count = 0
- print("BT_AI_key_press")
- EventMesh.publish("BT_AI_key_press")
-
- def pwk_key_release(self):
- self.check_rest_timer_count = 0
- #print("pwk_key_release")
- EventMesh.publish("pwk_key_release")
- def send_get_versions(self,event=None,msg=None):
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- #self.read_ack_flag = 0
- json_data = \
- {
-
- "event": "get_versions",
- }
- json_str =ujson.dumps(json_data)
- print("send_get_versions {}".format(json_str))
- self.send_data(json_str,1)
- #self.AI_uart.write(json_str)
- count =1000
- while True:
- count -=1
- if count <=0 or self.read_ack_flag == 1:
- break
- utime.sleep_ms(1)
-
- pass
- def start_ota(self,event=None,msg=None):#通过串口给蓝牙升级
- #EventMesh.publish("wakeup_BT")
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- print("start_ota!!!")
- print("start_ota!!!")
- json_data = \
- {
- "event": "ota_start",
- }
- json_str =ujson.dumps(json_data)
- self.AI_uart.write(json_str)
- print(json_str)
- count =100
- while True:
- count -=1
- if count <=0 or self.read_ack_flag == 1:
- break
- utime.sleep_ms(1)
- print("start_ota count:{}!!!".format(count))
-
- def end_ota(self,event=None,msg=None):#通过串口给蓝牙升级
- #EventMesh.publish("wakeup_BT")
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- print("end_ota!!!")
- print("end_ota!!!")
- json_data = \
- {
- "event": "ota_end",
- }
- json_str =ujson.dumps(json_data)
- self.AI_uart.write(json_str)
- print(json_str)
- count =100
- while True:
- count -=1
- if count <=0 or self.read_ack_flag == 1:
- break
- utime.sleep_ms(1)
- print("end_ota count:{}!!!".format(count))
-
- def bt_ota_run(self,event=None,msg=None):#通过串口给蓝牙升级
- # EventMesh.publish("wakeup_BT")
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- print("bt_ota_run!!!:{}".format(msg))
- print("bt_ota_run!!!")
- print("bt_ota_run!!!")
- self.bt_ota_flag = 1
- self.read_ack_flag = 0
-
- json_data = \
- {
- "event": "ota_start",
- }
- json_str =ujson.dumps(json_data)
- self.AI_uart.write(json_str)
- print(json_str)
- count =2000#等20秒,擦除flash
- while True:
- count -=1
- if count <=0 or self.read_ack_flag == 1:
- break
- utime.sleep_ms(10)
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- print("ota_start:count:{}".format(count))
- url = msg[0]
- bt_size = msg[1]
- headers_T = {"Content-Type":"application/octet-stream"}
- response = request.get(url = url,headers=headers_T,timeout = 120,decode = False)
- offset = 0
- checksum = 0
- for i in response.content:
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
-
-
- data = base64.b64encode(i)
- send_count = 5#最大重发5次,没ack或者返回error重发
- while 1:
- send_count-=1
- self.read_ack_flag = 0
- checksum = 0
- # 对每个字节进行异或运算
- for byte in i:
- checksum ^= byte # 等价于 checksum = checksum ^ byte
- json_data = \
- {
- "event": "ota_data",
- "update":data,
- "offset":offset,
- "checksum":checksum
- }
- print("offset:{},checksum:{}".format(offset,checksum))
- json_str =ujson.dumps(json_data)
- self.AI_uart.write(json_str)
- #utime.sleep_ms(100)
- count =300
- while True:
- count -=1
- if count <=0 or self.read_ack_flag != 0 :
- break
- utime.sleep_ms(10)
- print("ota_data:{} count:{}".format(len(json_str),count))
- if self.read_ack_flag == 1:
- break
- if send_count <=0 :
- return -1
- offset+=len(i)
- if offset < bt_size:
- return -1
- self.read_ack_flag = 0
- json_data = \
- {
- "event": "ota_end",
- }
- json_str =ujson.dumps(json_data)
- self.AI_uart.write(json_str)
-
- count =100
- while True:
- count -=1
- if count <=0 or self.read_ack_flag == 1:
- break
- utime.sleep_ms(1)
- self.bt_ota_flag = 0
- return 0
-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
- 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:
- ai_gpio = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22") or "GPIO22"
- 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)
- except Exception:
- ai_gpio = "GPIO22"
- pwk_gpio = "GPIO26"
- bt_wakeup_en = False
- wakeup_cta1_gpio = None
- wakeup_bt_gpio = None
-
- self.extint_AI = ExtInt(getattr(ExtInt, ai_gpio), ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_PU, self.keyAI_extint_cb)
- 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.extint_AI.enable()
- self.pwk_key_status = 0
- self.pwk_key_timer_count = 0
- self.pwk_key_long_press = 0
- self.pwk_key_press = 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
- 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):
- print("powerKey_callback:{}".format(status))
-
-
- 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("第一次按下,等待第二次按下")
-
- #第二次按下(双击判断)
- 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 >= 10*3: #长按3s
- #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 keyAI_extint_cb(self,args):#call 2
- try:
- #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.extint_AI.disable()
- 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("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.extint_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.extint_AI.enable()
- 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
- 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
- Vbatt = Power.getVbatt()
- 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
- _bcfg = _BatteryCfg.instance()
- 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)
- empty_soc = int(_bcfg.empty_soc)
- except Exception:
- low_soc, empty_soc = 30, 10
- 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 and soc is not None:
- empty_by_soc = soc < empty_soc
- empty_by_mv = Vbatt is not None and Vbatt < int(_bcfg.empty_mv)
- if empty_by_soc or empty_by_mv:
- if getattr(self, "_empty_shutdown_done", 0) == 0:
- self._empty_shutdown_done = 1
- try:
- print("[BAT] empty shutdown soc={} vbatt={}".format(soc, Vbatt))
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo", {"func": self._battery_empty_shutdown_seq})
- elif 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):
- # 返回 int 型 SoC(0~100):便于 TTS/上报/比较,且 OCV 插值本身比裸电压稳;不做小数避免频繁跳变
- try:
- Vbatt = Power.getVbatt()
- except Exception:
- return 0
- soc = _BatteryCfg.instance().soc_from_vbatt(Vbatt)
- if soc is None:
- return 0
- if soc < 0:
- return 0
- if soc > 100:
- return 100
- return int(soc)
- 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)
-
-#存储信息
-class getStorageDataManager(Abstract):#获取内存数据
- def __init__(self):
- self.config_data = {}
- check = ql_fs.path_exists("/usr/static/storage_file/config.json")
- if check == True:
- with open("/usr/static/storage_file/config.json","r") as fd:
- self.config_data = ujson.load(fd)
- else :
- self.config_data = {'old_time': 0, 'flip_sw': 0}
- pass
-
- def post_processor_after_instantiation(self):
- EventMesh.subscribe("set_storage_data", self.set_storage_data)
- EventMesh.subscribe("set_storage_config", self.set_storage_config)
- EventMesh.subscribe("get_storage_data", self.get_storage_data)
- EventMesh.subscribe("get_storage_config", self.get_storage_config)
-
- def set_storage_data(self, event, msg):
- # EventMesh.debug_log("set_storage_data:{}".format(msg))
- if msg != None:
- self.config_data = msg
- with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:
- ujson.dump(self.config_data,fd)
-
-
- def set_storage_config(self,event,msg):
- # EventMesh.debug_log("set_storage_key_data1:{}".format(msg))
- if len(msg) >= 2:
- self.config_data[msg[0]]=msg[1]
- # EventMesh.debug_log("set_storage_key_data2:{}".format(self.config_data[msg[0]]))
- EventMesh.publish("publish_message_event_ui",{"func":self.set_storage_save,"data":0})
- def set_storage_save(self, data):
- with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:
- ujson.dump(self.config_data,fd)
- def get_storage_data(self, event, msg):
- # EventMesh.debug_log("get_storage_data:{}".format(self.config_data))
- return self.config_data
- def get_storage_config(self,event,msg):
- # EventMesh.debug_log("get_storage_key_data1:{}".format(msg))
- b = msg in self.config_data.keys()
- if b == True:
- # EventMesh.debug_log("get_storage_key_data2:{}".format(self.config_data[msg]))
- return self.config_data[msg]
- else :
- return None
- # try:
- # return self.config_data[msg]
- # except:
- # EventMesh.debug_log("get_storage_key_data:none key:{}",msg)
- # return None
-#设备资源授权平台
-class device_res_author(Abstract):
-
- def __init__(self) :
- print("device_res_author __init__ start !!!!!!!!!!!!!!!!!")
- self.deviceId = modem.getDevImei()#"876226002022574"
- self.SHA1_BLOCK_SIZE = 64
- self.PAD_BLOCK_SIZE = 16
- self.SHA256 = uhashlib.sha256
- self.pad = lambda s: s + chr(self.PAD_BLOCK_SIZE - len(s) % self.PAD_BLOCK_SIZE) * (self.PAD_BLOCK_SIZE - len(s) % self.PAD_BLOCK_SIZE)
- # 除去补16字节的多余字符
- self.unpad = lambda s: s[:-s[-1]]
- #K01
- # self.key = b"MWrMZjdtcPhj41jWuoqFAhTmqyFluqj1"
- # self.pid = "qAtdDOGXgXYjT2HGFr3a"
- #G15
- # self.key = b"3LWfBicxfRZ3ZAqmZCFuvMdGUXdAFWkb"
- # self.pid = "qLx4Pu4mSUQxyfVKGLXG"
- #7pin
- # pid:YiMEGsAbHqYwhO3IRWVE
- # 产品密钥:yFXqKI3EvznGbKLb4mxuW2SErG5XyJwZ
- self.key = b"uKLrChwrxA1LklgBc3VwawYjUkbT3BgX"
- self.pid = "cqxJZi5CNgzjS3mX21Zw"
- # if dev_type == 'jswm' or dev_type == 'jswm_BOX':#金三完美
- # self.key = b"FuXG4ptpGbzvKbdB4vpUypisbENgy2nB"
- # self.pid = "DieBeXahZHLrjSnORbgN"
- self.dev_res_timer = osTimer()
- self.dev_res_timer.start(20000, 1, self.dev_timer_cb)
- self.dev_res_timerCount = 0
- self.ud_token_timer = osTimer()
- self.ud_token_timer.start(10000, 1, self.ud_token_timer_cb)
- self.ud_token_timer_Count = 0
- self.dev_token = None
- self.res_error_count = 0
- self.ntptime_flag = 0
- EventMesh.subscribe("get_dev_token", self.get_dev_token)
- EventMesh.subscribe("update_dev_token", self.update_dev_token)
- EventMesh.subscribe("dev_active", self.dev_active)
- EventMesh.subscribe("dev_sign_in", self.dev_sign_in)
- EventMesh.subscribe("dev_NTP", self.dev_NTP)
- EventMesh.subscribe("post_iccid_user", self.post_iccid_user)
- # self.post_iccidd_token_timer = osTimer()
- # self.post_iccidd_token/_timer.start(10000, 0, self.post_iccidd_token_timer_cb)
- self.connect_web_flag = 0
- #EventMesh.publish_async("dev_NTP",thread_size = 8192)
-
- # count = 0
- # while True:
- # if self.ntptime_flag == 1:
- # break
- # utime.sleep_ms(1)
- # count+=1
- # if count>600:
- # break
- # EventMesh.publish_async("dev_active",thread_size = 8192)
- # EventMesh.publish_async("dev_sign_in",thread_size = 8192)
-
- self.dev_NTP()#获取时间
- self.dev_active() #设备激活
- self.dev_sign_in() #设备登入
-
-
- print("device_res_author __init__ end !!!!!!!!!!!!!!!!!")
- def post_iccidd_token_timer_cb(self,*argc):
- EventMesh.publish("post_iccid_user")
-
- def get_dev_token(self, event, msg):
- return self.dev_token
- #网络校时
- def dev_NTP(self, event=None, msg=None):
- response = None
- try:
- print("dev_NTP1")
- url = constants.DMP_BASE + "/dev/time"
- print("dev_NTP:{}".format(url))
- response = request.get(url,timeout=5)
- data = response.json()
- print("dev_NTP:{}".format(data))
- if data:
- try:
- timeSecond = data["data"]["timestamp"]//1000
-
- next_alarm_time = utime.localtime(timeSecond)
- print("timeSecond {} next_alarm_time {}".format(timeSecond,next_alarm_time))
- machine.RTC().datetime((next_alarm_time[0],next_alarm_time[1],next_alarm_time[2],next_alarm_time[6],next_alarm_time[3],next_alarm_time[4],next_alarm_time[5],0))
- self.ntptime_flag = 1
- except Exception as e:
- usys.print_exception(e)
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
- print("dev_NTP end")
- def dev_timer_cb(self, *args): #设备资源管理鉴权平台定时器
- EventMesh.publish("publish_AI_btnInfo",{"func":self.dev_timer_cb_})
-
- def dev_timer_cb_(self):
- #if EventMesh.DEV_RES_MANAGE_AUTHEN_PLATFORM == 1:
- try:
- if self.dev_token == None:
- if self.ntptime_flag == 0:
- self.ntptime_flag = 1
- EventMesh.publish_async("dev_NTP",thread_size = 8192)
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.dev_NTP})
- print("timer:{}!!!!!!!!\r\n".format(utime.localtime()))
- EventMesh.publish_async("dev_active",thread_size = 8192)
- EventMesh.publish_async("dev_sign_in",thread_size = 8192)
- # self.dev_active() #设备激活
- # self.dev_sign_in() #设备登入
- self.dev_res_timerCount+=1
- if self.dev_res_timerCount > 3: #开机一分钟内没有获取到密码和token 等下一次更新令牌在走登入 激活流程
- self.dev_res_timerCount=0
- self.dev_res_timer.stop()
- except Exception as e:
- usys.print_exception(e)
- def ud_token_timer_cb(self, *args):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.ud_token_timer_cb_})
-
- def ud_token_timer_cb_(self):
- try:
- #print("ud_token_timer_!!!!")
- self.ud_token_timer_Count+=1
- if self.ud_token_timer_Count >= 720: #定时10*3000s时更新token
- self.ud_token_timer_Count = 0
- EventMesh.publish_async("dev_sign_in",thread_size = 8192)
- except Exception as e:
- usys.print_exception(e)
- def get_sim_Status(self):
- return sim.getStatus()
- def HMAC(self,k, m):
-
- KEY_BLOCK = k + (b'\0' * (self.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 + self.SHA256(inner_message).digest()
- return self.SHA256(outer_message)
- def get_hmac_sha256(self,k, m):
- data = self.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
- # 加密函数
- def aes_CBC_Encrypt(self,data, key, iv): # CBC模式的加密函数,data为明文,key为16字节密钥,iv为16字节的偏移量
- key = key.encode('utf-8')
- iv = iv.encode('utf-8') # CBC 模式下的偏移量
- data = self.pad(data) # 补位
- data = data.encode('utf-8')
- aes = ucryptolib.aes(key,ucryptolib.MODE_CBC,iv)
- result = base64.b64encode(aes.encrypt(data))
- return str(result, 'utf-8') # 以字符串的形式返回
-
- # 解密函数
- def aes_CBC_Decrypt(self,data, key, iv): # CBC模式的解密函数,data为密文,key为16字节密钥
- key = key.encode('utf-8')
- iv = iv.encode('utf-8')
-
- aes = ucryptolib.aes(key,ucryptolib.MODE_CBC,iv) #CBC模式
-
- # decrypt AES解密 B64decode为base64 转码
- result = aes.decrypt(base64.b64decode(data))
- result = self.unpad(result) # 除去补16字节的多余字符
- return str(result, 'utf-8') # 以字符串的形式返回
- #激活接口 没有密码走激活
- def dev_active(self, event=None, msg=None):
- signal = net.csqQueryPoll()
- if signal == -1 or self.get_sim_Status() != 1:#网络不正常
- print("dev_active 无 SIM卡!!!!!!!!\r\n")
- return
- print("dev_active 1\r\n")
- # 签名内容: = deviceId+pid+timeStamp
- dev_pass = ""
- dev_pass = EventMesh.publish("get_storage_config","dev_pass")
- print("dev_pass:{}\r\n".format(dev_pass))
- if dev_pass == None:
- dev_pass = ""
- if dev_pass == "" or dev_pass == None:
- try:
- date_tmp = utime.localtime() #获取当前时间
- print("date_tmp:{}\r\n".format(date_tmp)) #转换时间戳s
- timeStamp = (utime.mktime(date_tmp))*1000#获取时间戳ms
- deviceId = self.deviceId #"876226002022574" #以imei号
- pid = self.pid
- key = self.key
- sign_data = "{}{}{}".format(deviceId,pid,timeStamp) #签名内容
- print("sign_data:{}\r\n".format(sign_data))
- sha256_hex = self.get_hmac_sha256(key,sign_data)
- print("sha256_hex:{}\r\n".format(sha256_hex))
-
- data = {
- "deviceId":"{}".format(deviceId), #设备名
- "pid":pid, #产品PID
- "sign":"{}".format(sha256_hex), #HMACSHA256签名后的数据
- "timeStamp":timeStamp, #要以ms的形式
- "signMethod":"HMACSHA256" #签名方式 HMACSHA256
- }
- print(ujson.dumps(data))
- headers_T ={"Content-Type":"application/json"}
- url = constants.DMP_BASE + "/dev/auth/active"
- print("dev_active:{}".format(url))
- data =ujson.dumps(data)
- response = None
- response_result = 0
- try:
- response = request.put(url,data=data,headers = headers_T,timeout=5)
- response_result = 1
- except Exception as e:
- usys.print_exception(e)
- print("put error!!!!!\r\n")
- response_result = 0
-
- IV = "" #偏移量
- PASSWORD= "" #服务器返回后的加密内容 密码
- if response_result == 1: #访问成功ip
- if response.status_code == 200:
- print("headers:{}".format(response.headers))
- content = response.content
- c = next(content)
- # "api.xcloudtech.com"
- # d_server_port = 9000
- data = ujson.loads(c)
- # data = response.json()
- print("data-{}".format(data))
- if data != None:
- if "success" in data:
- if data['success'] == True:
- if "data" in data:
- data_tmp = data["data"]
- if "nonce" in data_tmp: #偏移量
- IV = data_tmp["nonce"]
- if "password" in data_tmp:
- PASSWORD = data_tmp["password"]
- # EventMesh.publish("set_storage_config",["dev_pass",PASS])
- if data["code"] == 20001:#鉴权成功
- pass
- elif data["code"] > 50000:#
- self.res_error_count+=1
- if self.res_error_count>=3:
- self.res_error_count = 0
-
- EventMesh.publish("paly_device_res_error")
- if data["code"] == 50003:#时间错误
- self.ntptime_flag = 0
- if PASSWORD != None and IV != None:
- if len(PASSWORD) != 0 and len(IV) != 0:
- password = PASSWORD
- iv = IV
- #解密 之后保存本地下一次登入做使用
- decrypt_data = self.aes_CBC_Decrypt(password, self.key, iv) #对服务器下发的密码进行解密
- print("decrypt_data {}".format(decrypt_data))
- EventMesh.publish("set_storage_config",["dev_pass",decrypt_data])
- print("IV:{}\r\n PASS:{}".format(IV,PASSWORD))
- if response != None:
- response.close()
- except Exception as e:
- usys.print_exception(e)
- print("dev_active end\r\n")
- return [response_result,PASSWORD,IV]
- #随机生成16个字节的字符串作为加密的IV 偏移量
- def generate_random_string(self,length=16):
- random_str = ""
- chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
- for i in range(length):
- random_str+=chars[random.randint(0,length)]
- return random_str
-
- #设备登入 没有Token 走登入获取token
- def dev_sign_in(self, event=None, msg=None):
-
-
- signal = net.csqQueryPoll()
- if signal == -1 or self.get_sim_Status() != 1:#网络不正常
- print("dev_sign_in 无 SIM卡!!!!!!!!\r\n")
- return
- print("dev_sign_in 1")
- token = ""
- try:
- token = self.dev_token
- dev_pass = EventMesh.publish("get_storage_config","dev_pass")
- #print("token:{}\r\n".format(token))
- if token == None:
- token = ""
- if dev_pass == None:
- dev_pass = ""
- if len(dev_pass) == 0:#没有密码
- return
- #if token == None or token == "":
- if 1: #返回结果成功
- #获取本地保存的密码
- decrypt_data=EventMesh.publish("get_storage_config","dev_pass")
- # decrypt_data = self.aes_CBC_Decrypt(password, self.key, iv) #对服务器下发的密码进行解密
- print("decrypt_data {}".format(decrypt_data))
- #随机生成16个字符串 设备生成做加密使用
- random_string = self.generate_random_string(16) #随机的16个字符串
- enc_iv = random_string #加密的IV偏移量
- #设备端进行密码加密
- encrypt_data = self.aes_CBC_Encrypt(decrypt_data, self.key, enc_iv)
- print("encrypt_data {}".format(encrypt_data))
-
- #登入数据
- deviceId = self.deviceId
- sign_in_data = {
- "deviceId":"{}".format(deviceId), #设备ID
- "password":"{}".format(encrypt_data), #加密的数据
- "nonce":"{}".format(random_string) #随机的16个字符串
- }
- print(ujson.dumps(sign_in_data))
- sign_url =None
- sign_url = constants.DMP_BASE + "/dev/auth/sign_in"
- #获取验证码
- #url = "http://dmp.qipai.com.cn:30461/dev/device/bind-code/860132066060377"
- # token = EventMesh.publish("get_dev_token")
- # headers_T = {"Content-Type":"application/x-www-form-urlencoded","Accept":"*/*","bz-dt":"{}".format(token)}
- # response = request.get(url,headers = headers_T,timeout=5)
- # data = response.json()
- print("dev_sign_in:{}".format(sign_url))
- headers_T ={"Content-Type":"application/json"}
- response_result = 0
- response = 0
- try:
- response = request.post(sign_url,data=ujson.dumps(sign_in_data),headers = headers_T,timeout=5)
- response_result = 1
- except Exception as e:
- usys.print_exception(e)
- print("post error!!!!!\r\n")
- response_result = 0
- # {'msg': '', 'code': 20001, 'success': True,
- # 'data': {'deviceId': '876226002022574',
- # 'token': '95436a9e09fd0ff89feb8825257dca982f'}
- # }
-
- #self.dev_token = ""
- if response_result == 1:
- if response.status_code == 200:
- content = response.content
- c = next(content)
- data = ujson.loads(c)
- print("data-{}".format(data))
- if data != None:
- if "success" in data:
- if data['success'] == True: #登入成功
- if "data" in data:
- data_tmp = data["data"]
- if "token" in data_tmp: #token
- self.dev_token = data_tmp["token"]
- else:#报错清空密钥
- print("登录失败,清空密钥")
- EventMesh.publish("set_storage_config",["dev_pass",""])
- if data["code"] == 50003:#时间错误
- self.ntptime_flag = 0
-
- print("token:{}\r\n".format(self.dev_token))
- if response != None:
- response.close()
- pass
- except Exception as e:
- print("post error!!!!!\r\n")
- response_result = 0
- if EventMesh.publish("get_dev_token") != None:
- if len(EventMesh.publish("get_dev_token")) != 0:
- if self.connect_web_flag == 0:
- EventMesh.publish("post_iccid_user")
- self.connect_web_flag = 1
- print("dev_sign_in end")
- #ICCID上传
- def post_iccid_user(self,event=None,msg=None):
- response = None
- try:
- iccid = sim.getIccid()
- if iccid != -1 and len(iccid) > 5:
- print("pots_ICCID:{}".format(iccid))
- #Headers = {'Transfer-Encodong':'chunked'}
- token = EventMesh.publish("get_dev_token")
- headers_T = None
- if token:
- headers_T = {"qp-dt":"{}".format(token)}
- #http://dmp.raisound.com/bz-api/dev/iccid?deviceId=866610001099679&iccid=89860002192307889057
- url =None
- url = (constants.DMP_BASE + "/dev/iccid?deviceId={}&iccid={}").format(modem.getDevImei(),iccid)
- print("{} H:{}".format(url,headers_T))
- response = request.post(url,headers = headers_T)
- if response.status_code == 200:
- data = response.json()
- print("pots_ICCID:{}".format(data))
- else:
- print("pots_ICCID error!!!!")
- for i in response.text:
- print(i)
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
- #获取新的令牌 更新token
- def update_dev_token(self, event=None, msg=None):
- print("update_dev_token")
- try:
- dev_pass = ""
- dev_pass = EventMesh.publish("get_storage_config","dev_pass")
-
- if dev_pass == None:
- dev_pass = ""
- if len(dev_pass) == 0: #没有密码走激活
- print("no dev_pass:{}\r\n".format(dev_pass))
- EventMesh.publish("dev_active")
- token = self.dev_token
- if token == None:
- token = ""
- if len(token) == 0: #没有token走登入
- print("no dev_token:{}\r\n".format(token))
- #self.dev_sign_in()
- EventMesh.publish("dev_sign_in")
- dev_tokenip =None
- dev_tokenip = constants.DMP_BASE + "/dev/auth/token"
-
- token = self.dev_token
- if token == None:
- token = ""
- if len(token) != 0:
- response_result=0
- response = None
- headers_T = {"bz-dt":"{}".format(token)}
- try:
- response = request.get(dev_tokenip,headers = headers_T,timeout=5)
- response_result = 1
- except Exception as e:
- print("get error!!!!!\r\n")
- response_result = 0
- if response_result == 1:
- if response.status_code == 200:
- try:
- headers_data = response.headers
- # print("headers_data!!!!! headers_data:{}\r\n".format(headers_data))
- if "bz-dt" in headers_data: #bz-dt字段是否在头部数据里面 如果有就需要更新
- bz_dt = headers_data["bz-dt"]
- self.dev_token = bz_dt.replace(" ", "") #去掉所有的空格符
- print("update_dev_token:{}".format(self.dev_token))
- except Exception as e:
- print("headers_data!!!!! error\r\n")
- if response != None:
- response.close()
- except Exception as e:
- usys.print_exception(e)
- print("update_dev_token end")
-
-
-
-
-#OTA升级
-class OTA():
- def __init__(self):
- #EventMesh.subscribe("min_sys_ota",self.min_sys_ota)
- self.downloadFileVoURL1= ""
- self.downloadFileVoURL2= ""
- self.downloadFileVoURLBT=""
- self.BT_ota_size =0
- self.ota_task_id = None
- #蓝牙升级升级,返回0,没有升级;返回,1有升级,正在执行升级;返回2,电压不够
- def bt_ota(self,event=None,msg=None):
- print("bt_ota_ota")
- signal = net.csqQueryPoll()
- if signal == -1 or sim.getStatus() != 1:#网络不正常
- print("bt_ota 无 SIM卡!!!!!!!!\r\n")
- return 0
- # return 0
- response = None
- ret_str =0
-
- self.downloadFileVoURLBT=""
- try:
- #sourceVersion = versions.Build_Time
- # sourceVersion = "7PIN-"+sourceVersion[:10]+"-"+sourceVersion[11:-3]
- # print("sourceVersion:{}".format(sourceVersion))
- # sourceVersion = sourceVersion.replace(" ", "0")
- # print("sourceVersion2:{}".format(sourceVersion))
- sourceVersion = version_util.get_unified_version()
- print("sourceVersion:{}".format(sourceVersion))
- bt_versions = EventMesh.publish("get_bt_versions")
- if bt_versions == None:
- bt_versions=""
- print("bt_versions:{}".format(bt_versions))
- #url = "http://iot.api.thisiot.com/ota/task?deviceId=860132060348836&sourceVersion=2024-12-20-15:35-1"
- url =None
- url = "https://dmp-api.qipai.com/qp-dmp-api/ota/task?deviceId={}&sourceVersion={}".format(modem.getDevImei(),sourceVersion)
- print(url)
- response = request.get(url,timeout=5)
- if response.status_code == 200:
- data = response.json()
- print("OTA升级请求:{}".format(data))
-
- if data:
- try:
- if data["code"] == 20007:#有新版本
- if data["data"]["targetVersion"] != bt_versions and "fileList"in data["data"]:
- #if len(data["data"]["fileList"]) == 2:
- for fileList in data["data"]["fileList"]:
-
- if fileList["fileName"] == "update.ufw":
- self.BT_ota_size = fileList["size"]
- self.downloadFileVoURLBT = fileList["url"]
- if data["code"] == 20006:#已是最新版本
- ret_str = 0
-
- except Exception as e:
- usys.print_exception(e)
- else:
- print("bt_ota get error")
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
-
- print("downloadFileVoURLBT{}".format(self.downloadFileVoURLBT))
- print("bt_ota end")
- if self.downloadFileVoURLBT !="":
- battery = EventMesh.publish("get_battery")
- if battery>=40:
- self.ota_timer = osTimer()
- #self.ota_timer.start(15000, 0, self.ota_timer_cb)
- if self.ota_task_id ==None:
- self.ota_task_id = _thread.start_new_thread(self.ota_task,())
- ret_str = 1
- else:
- ret_str = 2
- return ret_str
- #小系统升级,返回0,没有升级;返回,1有升级,正在执行升级;返回2,电压不够
- def min_sys_ota(self,event=None,msg=None):
- print("min_sys_ota")
- signal = net.csqQueryPoll()
- if signal == -1 or sim.getStatus() != 1:#网络不正常
- print("min_sys_ota 无 SIM卡!!!!!!!!\r\n")
- return 0
- # return 0
- response = None
- ret_str =0
- self.downloadFileVoURL1= ""
- self.downloadFileVoURL2= ""
- #self.downloadFileVoURLBT=""
- try:
- sourceVersion = version_util.get_unified_version()
- print("sourceVersion:{}".format(sourceVersion))
- url =None
- url = "https://dmp-api.qipai.com/qp-dmp-api/ota/task?deviceId={}&sourceVersion={}".format(modem.getDevImei(),sourceVersion)
- print(url)
- response = request.get(url,timeout=5)
- if response.status_code == 200:
- data = response.json()
- print("OTA升级请求:{}".format(data))
-
- if data:
- try:
- if data["code"] == 20007:#有新版本
- if "fileList"in data["data"]:
- #if len(data["data"]["fileList"]) == 2:
- for fileList in data["data"]["fileList"]:
- if fileList["fileName"] == "dfota_2.bin":
- self.downloadFileVoURL2 = fileList["url"]
- elif fileList["fileName"] == "dfota_1.bin":
- self.downloadFileVoURL1 = fileList["url"]
- elif fileList["fileName"] == "update.ufw":
- self.BT_ota_size = fileList["size"]
- self.downloadFileVoURLBT = fileList["url"]
- if data["code"] == 20006:#已是最新版本
- ret_str = 0
-
- except Exception as e:
- usys.print_exception(e)
- else:
- print("min_sys_ota get error")
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
-
- print("min_sys_ota:downloadFileVoURL1:{},\ndownloadFileVoURL2:{},\ndownloadFileVoURLBT{}".format(self.downloadFileVoURL1,self.downloadFileVoURL2,self.downloadFileVoURLBT))
- print("min_sys_ota end")
- if self.downloadFileVoURL2!="" and self.downloadFileVoURL1!="" :
- battery = EventMesh.publish("get_battery")
- if battery>=40:
- self.ota_timer = osTimer()
- #self.ota_timer.start(15000, 0, self.ota_timer_cb)
- if self.ota_task_id ==None:
- self.ota_task_id = _thread.start_new_thread(self.ota_task,())
- ret_str = 1
- else:
- ret_str = 2
- return ret_str
-
- # def ota_timer_cb(self, *args, **kwargs):
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.usb_event})
-
- def ota_task(self):
- import fota
- utime.sleep_ms(15000)
- count = 30#最多等待30秒
- while 1:
- count-=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 or count<=0 or voice_record_satrt_flag == 0:
- break
- utime.sleep_ms(100)
- if self.downloadFileVoURLBT != "" and self.BT_ota_size != 0:
- count = 5 #重试5次
- while 1:
- count -=1
- ret = EventMesh.publish("bt_ota_run",[self.downloadFileVoURLBT,self.BT_ota_size ]) #升级蓝牙
-
- if ret == 0 or count <=0:
- break
- utime.sleep_ms(1000)
- utime.sleep_ms(1000)
- #EventMesh.publish("rest_TB") #复位蓝牙
- utime.sleep_ms(50000)
- if self.downloadFileVoURL1 == "" and self.downloadFileVoURL2 == "": #cat1没有升级的时候,要重启设备
- Power.powerRestart()
- if self.downloadFileVoURL1 != "" and self.downloadFileVoURL2 != "":
- fota_obj = fota()
- fota_obj.httpDownload(url1=self.downloadFileVoURL1,url2=self.downloadFileVoURL2)
- #ota升级结果返回,开机时调用一次即可
- def ota_result(self,event=None,msg=None):
- response = None
- try:
- currentVersion = version_util.get_unified_version()
- print("currentVersion:{}".format(currentVersion))
- url = (constants.DMP_BASE + "/ota/result?deviceId={}¤tVersion={}").format(modem.getDevImei(),currentVersion)
- print(url)
- response = request.post(url,timeout=5)
- if response.status_code == 200:
- data = response.json()
- print("OTA升级结果上报:{}".format(data))
- else:
- print("ota_result error!!!!")
- for i in response.text:
- print(i)
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
- #ICCID上传
- def pots_ICCID(self,event=None,msg=None):
- response = None
- try:
- iccid = sim.getIccid()
- if iccid != -1 and len(iccid) > 5:
- print("pots_ICCID:{}".format(iccid))
- #Headers = {'Transfer-Encodong':'chunked'}
- token = EventMesh.publish("get_dev_token")
- headers_T = None
- if token:
- headers_T = {"bz-dt":"{}".format(token)}
- #http://dmp.raisound.com/bz-api/dev/iccid?deviceId=866610001099679&iccid=89860002192307889057
- url =None
- url = (constants.DMP_BASE + "/dev/iccid?deviceId={}&iccid={}").format(modem.getDevImei(),iccid)
- print("{} H:{}".format(url,headers_T))
- response = request.post(url,headers = headers_T,timeout=5)
- if response.status_code == 200:
- data = response.json()
- print("pots_ICCID:{}".format(data))
- else:
- print("pots_ICCID error!!!!")
- for i in response.text:
- print(i)
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
-# 原 VC_State_* 整型已移除;会话/轮次见 VCTurnState / VCStateMachine(含 tts_rx_phase、speech_uplink_open)。
-
-VC_record_TYPE_PCM = 0
-VC_record_TYPE_AMR = 1
-VC_play_TYPE_PCM = 0
-VC_play_TYPE_MP3 = 1
-
-#websocket发送数据编号
-# AI_client_asr_num = 0
-# AI_client_chat_num = 1
-# AI_client_tts_num = 2
-AI_client_VC_num = 3
-AI_client_asr_num = 4
-
-
-class VCSessionState:
- Disconnected = 0
- Connecting = 1
- Starting = 2
- Started = 3
-
-
-class VCTurnState:
- Idle = 0
- Listening = 1
- SpeechStreaming = 2
- SpeechEnded = 3
- Thinking = 4
- TtsReceiving = 5
- Playing = 6
- AwaitPlayedAck = 7
-
-
-class VC_TtsRxPhase:
- Idle = 0
- Started = 1
- ServerCompleted = 2
-
-
-class VCMode:
- Realtime = 0 # VAD on(长对话,协议:无需 audio.create/completed)
- PushToTalk = 1 # VAD off(单对话,协议:必须 audio.create/completed 包裹音频帧)
-
-
-class VCStateMachine:
- # 轻量状态机骨架(先落地骨架与动作接口,后续逐步把旧 if/flag 逻辑收敛进来)。
- #
- # 约束来自《金三服务-语音对话V2.5》:
- # - 收到 started 后才允许发音频帧(否则 session not initialized)
- # - VAD 开启:不需要 audio.create/completed
- # - VAD 关闭:必须 audio.create → frames → audio.completed
- # - 播放结束 audio.played 是进入下一轮的门控信号之一(尤其关键词打断场景)
-
- def __init__(self, vc):
- self.vc = vc
- self.session = VCSessionState.Disconnected
- self.turn = VCTurnState.Idle
- self.last_llm_ms = 0
- self.last_tts_pending_ms = 0
- self.await_played = False
- self.last_tts_completed_ms = 0
- self.tts_rx_phase = VC_TtsRxPhase.Idle
- self.speech_uplink_open = 0
-
- def reset_rx_phases(self):
- self.tts_rx_phase = VC_TtsRxPhase.Idle
- self.speech_uplink_open = 0
-
- def mark_tts_rx_idle(self):
- self.tts_rx_phase = VC_TtsRxPhase.Idle
-
- def on_server_asr_started(self):
- if self.session == VCSessionState.Started:
- self.speech_uplink_open = 1
- self.log("server.asr.started")
-
- def _turn_name(self, t):
- try:
- return {
- VCTurnState.Idle: "Idle",
- VCTurnState.Listening: "Listening",
- VCTurnState.SpeechStreaming: "SpeechStreaming",
- VCTurnState.SpeechEnded: "SpeechEnded",
- VCTurnState.Thinking: "Thinking",
- VCTurnState.TtsReceiving: "TtsReceiving",
- VCTurnState.Playing: "Playing",
- VCTurnState.AwaitPlayedAck: "AwaitPlayedAck",
- }.get(t, str(t))
- except Exception:
- return str(t)
-
- def _session_name(self, s):
- try:
- return {
- VCSessionState.Disconnected: "Disconnected",
- VCSessionState.Connecting: "Connecting",
- VCSessionState.Starting: "Starting",
- VCSessionState.Started: "Started",
- }.get(s, str(s))
- except Exception:
- return str(s)
-
- def log(self, reason=""):
- # 只用于关键节点的单行状态日志,便于串口快速定位状态漂移
- try:
- print("[FSM] {} session={} turn={} await_played={} tts_rx={} speech_up={}".format(
- reason,
- self._session_name(self.session),
- self._turn_name(self.turn),
- 1 if self.await_played else 0,
- int(self.tts_rx_phase),
- 1 if self.speech_uplink_open else 0,
- ))
- except Exception:
- pass
-
- def mode(self):
- try:
- return VCMode.Realtime if self.vc.VC_mode == 0 else VCMode.PushToTalk
- except Exception:
- return VCMode.Realtime
-
- def on_session_start_request(self):
- self.session = VCSessionState.Starting
- self.turn = VCTurnState.Idle
- # 新会话开始时必须清空上一轮“待播完”状态,否则会出现:
- # - 切换模式/重连后 await_played 仍为 True → play_end 逻辑抢跑/不触发 → 无法自动开麦
- self.await_played = False
- self.last_tts_completed_ms = 0
- self.last_llm_ms = 0
- self.reset_rx_phases()
-
- def on_server_started(self):
- self.reset_rx_phases()
- self.session = VCSessionState.Started
- # 新会话 started:确保进入 Listening,且清理上轮播放门控
- self.await_played = False
- self.last_tts_completed_ms = 0
- if self.turn in (VCTurnState.Idle, VCTurnState.AwaitPlayedAck):
- self.turn = VCTurnState.Listening
- self.log("server.started")
-
- def on_server_vad_silence(self):
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.SpeechEnded
- self.log("server.vad.silence")
-
- def on_server_llm_created(self):
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.Thinking
- try:
- self.last_llm_ms = utime.ticks_ms()
- except Exception:
- self.last_llm_ms = 1
- self.log("server.llm.created")
-
- def on_server_tts_started(self):
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.TtsReceiving
- self.tts_rx_phase = VC_TtsRxPhase.Started
- self.log("server.tts.started")
-
- def on_server_tts_completed(self):
- if self.session == VCSessionState.Started:
- # tts.completed 只表示服务端音频下发结束,本地可能仍在播放;保持 Playing,由 await_played 门控播完上报
- self.await_played = True
- self.tts_rx_phase = VC_TtsRxPhase.ServerCompleted
- try:
- self.last_tts_completed_ms = utime.ticks_ms()
- except Exception:
- self.last_tts_completed_ms = 1
- self.log("server.tts.completed")
-
- def on_server_canceled(self):
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.Listening
- self.reset_rx_phases()
- self.log("server.canceled")
-
- def on_server_completed(self):
- if self.session == VCSessionState.Started:
- # 本轮结束:默认回到 Listening;是否自动开麦由 action 门控决定
- # 若仍在等待本地播完(await_played),completed 不应把 turn 拉回 Listening,
- # 否则会触发 completed→uplink_start 抢跑,打断播放链路。
- if not self.await_played and self.turn != VCTurnState.AwaitPlayedAck:
- self.turn = VCTurnState.Listening
- self.log("server.completed")
-
- def on_server_event(self, event, jsload_data=None):
- # 事件→动作(action list)适配层。
- # 现阶段只产出“可幂等的控制动作”(uplink start/stop),避免和旧逻辑重复触发播放等副作用。
- # 后续会逐步把更多行为(play_start/audio.played/ptt create/completed)迁移到这里。
- actions = []
- try:
- # LLM 阶段必须驱动 FSM.turn,否则 turn 会停在 SpeechEnded,导致:
- # - _fsm_timeout_table_tick 里「Thinking 超时 / synthesis 超时」永不命中
- # - 与注释和 force_tts 前置条件不一致
- if event == "llm.created":
- self.on_server_llm_created()
- return actions
- if event == "llm.completed":
- # 若对端偶发跳过 llm.created,用 completed 把 turn 补到 Thinking,避免兜底全失效
- try:
- if self.session == VCSessionState.Started:
- if self.turn in (VCTurnState.SpeechEnded, VCTurnState.Listening):
- self.turn = VCTurnState.Thinking
- except Exception:
- pass
- return actions
- if event == "started":
- self.on_server_started()
- return actions
- if event == "vad.silence":
- self.on_server_vad_silence()
- actions.append(("uplink_stop", "vad.silence"))
- return actions
- if event == "tts.started":
- self.on_server_tts_started()
- actions.append(("uplink_stop", "tts.started"))
- actions.append(("play_start", "tts.started"))
- return actions
- if event == "tts.completed":
- self.on_server_tts_completed()
- # 播放可能已在 started 时开启,这里不再强制 play_start,避免反复 open PCM
- return actions
- if event == "canceled":
- self.on_server_canceled()
- # canceled:仅长对话允许“立刻回到聆听并尝试开麦”(由 _uplink_request_start 门控保证安全幂等)
- try:
- if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0:
- actions.append(("uplink_start", "canceled"))
- except Exception:
- pass
- return actions
- if event == "completed":
- self.on_server_completed()
- # completed:若不在等待 audio.played(说明本轮无 TTS),仅长对话可直接进入下一轮聆听
- try:
- if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0:
- if (not getattr(self, "await_played", False)) and self.turn != VCTurnState.AwaitPlayedAck:
- actions.append(("uplink_start", "completed"))
- except Exception:
- pass
- return actions
- except Exception:
- pass
- return actions
-
- def on_local_audio_played(self):
- # 本地播报完成后:长对话回到 Listening,等待下一轮输入
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.Listening
- self.await_played = False
- self.last_tts_completed_ms = 0
- self.mark_tts_rx_idle()
- self.log("local.audio_played")
-
- def on_local_play_start(self):
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.Playing
- self.log("local.play_start")
-
- def on_local_synthesis_sent(self):
- # synthesis 发出后到 tts.started 之间:视为 TTS 接收阶段,禁止开麦
- if self.session == VCSessionState.Started:
- self.turn = VCTurnState.TtsReceiving
- self.log("local.synthesis_sent")
-
- def on_local_play_end(self):
- actions = []
- try:
- if self.session != VCSessionState.Started:
- return actions
- if self.await_played:
- actions.append(("send_audio_played", "local.play_end"))
- # 长对话模式:对话过程中的 TTS 播完后,自动开麦继续当前对话。
- # 例外:
- # - 刚切到长对话模式的提示音(mode_switch_long)不应自动开麦
- # - 其它提示/提醒(prompt)不应自动开麦
- # 实际开麦是否允许由 _uplink_request_start 的门控(FSM.turn / record_state 等)保证安全幂等。
- try:
- if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0:
- k = getattr(self.vc, "_last_tts_kind", "") or ""
- if k == "dialog":
- # 用户短按打断(RequestToSpeak):等 canceled 再开麦,勿 play_end 抢跑
- if getattr(self.vc, "_vc_pending_interrupt_uplink", 0) != 1:
- actions.append(("uplink_start", "local.play_end"))
- except Exception:
- pass
- self.on_local_audio_played()
- except Exception:
- pass
- self.log("local.play_end")
- return actions
-
-
-class AI_VC(Abstract):
- def get_vc_tts_playback_active(self, *args, **kwargs):
- try:
- return 1 if self._vc_tts_playback_active() else 0
- except Exception:
- return 0
-
- def get_vc_stream_active(self, *args, **kwargs):
- # 供本地提示音侧查询:TTS 流播放期间本地不可抢占。
- try:
- if self._vc_tts_playback_active():
- return 1
- except Exception:
- pass
- try:
- if getattr(self.fsm, "await_played", False):
- return 1
- except Exception:
- pass
- try:
- if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:
- return 1
- except Exception:
- pass
- try:
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
- return 1
- except Exception:
- pass
- try:
- if getattr(self, "_tts_pending", 0) == 1:
- return 1
- except Exception:
- pass
- return 0
-
- def _vc_fsm_log_snip(self):
- try:
- f = self.fsm
- return "sess={} turn={} tts_rx={} await={} sp_up={}".format(
- int(getattr(f, "session", -1)),
- int(getattr(f, "turn", -1)),
- int(getattr(f, "tts_rx_phase", -1)),
- 1 if getattr(f, "await_played", False) else 0,
- 1 if getattr(f, "speech_uplink_open", 0) else 0,
- )
- except Exception:
- return "fsm=?"
-
- def _vc_tts_pipeline_busy(self):
- try:
- if getattr(self.fsm, "await_played", False):
- return True
- if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:
- return True
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
- return True
- if getattr(self, "_tts_pending", 0) == 1:
- return True
- if getattr(self, "VC_Play_flag", 0) == 1:
- return True
- if self._vc_local_play_busy():
- return True
- except Exception:
- pass
- return False
-
- def _vc_local_play_busy(self):
- try:
- if getattr(self, "_vc_local_play_active", 0) != 1:
- return False
- until = getattr(self, "_vc_local_play_until_ms", 0)
- if until and utime.ticks_diff(until, utime.ticks_ms()) > 0:
- return True
- self._vc_local_play_active = 0
- self._vc_local_play_until_ms = 0
- except Exception:
- pass
- return False
-
- def _vc_capture_allowed(self, reason=""):
- """采集/上行开麦前:下行播放或本地短音未结束则禁止。"""
- try:
- if self._vc_tts_pipeline_busy():
- return False
- if self._vc_local_play_busy():
- return False
- except Exception:
- return False
- return True
-
- def _vc_stop_downlink_for_interrupt(self, reason="interrupt"):
- """详设 DES-SCN-003:打断/取消时先本地停播清队列,再允许开麦。"""
- try:
- if getattr(self, "AI_audio_queue", None) is not None:
- if self.AI_audio_queue.empty() != True:
- self.AI_audio_queue.clear()
- except Exception:
- pass
- try:
- if self._vc_tts_pipeline_busy() or getattr(self, "is_ruppt_tts", 0) == 1:
- self._drop_tts_audio = 1
- except Exception:
- pass
- try:
- self._ptt_hard_mute_local_playback()
- 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._vc_local_play_active = 0
- self._vc_local_play_until_ms = 0
- except Exception:
- pass
- try:
- self.fsm.await_played = False
- self._need_audio_played_ack = 0
- self._tts_pending = 0
- self._tts_pending_ms = 0
- except Exception:
- pass
- try:
- pending_intr = getattr(self, "_vc_pending_interrupt_uplink", 0) == 1
- if (
- not pending_intr
- and getattr(self, "VC_record_state_flag", 0) != 0
- and getattr(self, "record_data_save_flag", 0) == 0
- ):
- self._uplink_request_stop("stop_downlink/" + str(reason))
- except Exception:
- pass
-
- 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)
- 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:
- hold = 200
- self._vc_local_play_active = 1
- self._vc_local_play_until_ms = utime.ticks_add(utime.ticks_ms(), hold)
- except Exception:
- pass
- try:
- self.aud.play(play_mode, loop, path)
- except Exception as e:
- try:
- self._vc_local_play_active = 0
- self._vc_local_play_until_ms = 0
- except Exception:
- pass
- usys.print_exception(e)
-
- def _vc_tts_playback_active(self):
- """仅服务端 TTS/PCM 流在播(VC_Play_Start);aud.play 本地短音不算。"""
- try:
- if getattr(self, "VC_Play_flag", 0) == 1:
- return True
- except Exception:
- pass
- try:
- if getattr(self, "play_pcm", None) is not None:
- return True
- except Exception:
- pass
- return False
-
- def _vc_vad_uplink_stop_eligible(self):
- # 对应旧 VC_state >= SpeechStart
- try:
- if getattr(self.fsm, "speech_uplink_open", 0):
- return True
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- return t not in (VCTurnState.Idle, VCTurnState.Listening)
- except Exception:
- return False
-
- def _vc_long_idle_active_speech_or_later(self):
- # 对应旧 VC_state >= SpeechIng
- try:
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- return t not in (VCTurnState.Idle, VCTurnState.Listening)
- except Exception:
- return False
-
- def _vc_is_business_idle(self):
- # Level2:LED ai_state 为 idle,且无录音/流播/音乐闹钟等(与 WS ping 无关)
- try:
- if getattr(self, "music_play_flag", 0) == 1:
- return False
- if getattr(self, "alarm_play_flag", 0) == 1:
- return False
- except Exception:
- pass
- try:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- return False
- if getattr(self, "VC_Play_flag", 0) != 0:
- return False
- except Exception:
- pass
- try:
- if EventMesh.publish("get_vc_stream_active") == 1:
- return False
- except Exception:
- pass
- try:
- st = EventMesh.publish("get_led_debug_state")
- if st and isinstance(st, dict):
- if (st.get("ai") or "") != "idle":
- return False
- except Exception:
- return False
- return True
-
- def _vc_idle_deep_sleep_impl(self):
- try:
- _need_close = (self.VC_satrt_flag == 1) or (getattr(self, "AI_client_VC_isconnect", 0) == 1)
- print("[VC][idle_L2] 触发休眠流程 need_Close={} VC_mode={}".format(
- 1 if _need_close else 0, getattr(self, "VC_mode", -1)))
- except Exception:
- pass
- try:
- if self.VC_satrt_flag == 1 or getattr(self, "AI_client_VC_isconnect", 0) == 1:
- try:
- print("[VC][idle_L2] 调用 Close 断开会话/链路")
- except Exception:
- pass
- self.Close()
- except Exception:
- pass
- try:
- utime.sleep_ms(100)
- except Exception:
- pass
- try:
- print("[VC][idle_L2] 发布 Hsensor_set_sleep")
- EventMesh.publish("Hsensor_set_sleep")
- except Exception:
- pass
- try:
- print("[VC][idle_L2] 发布 SYSTEM_SLEEP ({})".format(constants.EVENT.SYSTEM_SLEEP))
- EventMesh.publish(constants.EVENT.SYSTEM_SLEEP)
- except Exception:
- pass
-
- def _vc_rx_resets_long_idle_timer(self, recv_data):
- """是否应因本帧下行清零 close_timerout(排除保活/心跳,含 V2.5 文本 ping 对端应答)。"""
- try:
- if recv_data is None:
- return False
- if isinstance(recv_data, (bytes, bytearray)):
- return True
- if not isinstance(recv_data, str):
- return False
- t = recv_data.strip()
- if not t:
- return False
- tl = t.lower()
- if tl in ("ping", "pong"):
- return False
- try:
- jd = ujson.loads(t)
- except Exception:
- return True
- ev = jd.get("event")
- if ev is None or ev == "":
- ev = jd.get("type")
- if ev is None:
- return True
- if str(ev).strip().lower() in _VC_KEEPALIVE_RX_EVENTS:
- return False
- return True
- except Exception:
- return True
-
- # -------------------------------------------------------------------------
- # 电源键 / AI 键:按 VC_mode 拆函数,便于阅读与修改(长对话 vs 单对话 PTT)
- # -------------------------------------------------------------------------
- def _handle_pwk_short_release(self, event=None, msg=None):
- """电源键短按:读留言标志、退会议/翻译;否则 Close 结束对话(清 sid,下次再 Start)。"""
- print("pwk_key_release")
- if EventMesh.publish("get_recv_voice") == 1:
- EventMesh.publish("update_play_new_voice_info")
- self.recv_voice = 0
- EventMesh.publish("set_history_voice_flag", 0)
- self.AI_new_voice_url = ""
- if self.trans_flag == 1 or self.meet_flag == 1:
- EventMesh.publish("exit_meet_mode")
- EventMesh.publish("exit_trans_mode")
- return
- EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
- EventMesh.publish("stop_sos_call")
-
- def _ai_key_press_ptt_mode(self):
- """单对话 PTT:短按仅停 TTS(不换 sid);非播放态短按吞掉;长按由 AI_key_long_press→ptt_begin。"""
- if self.VC_mode != 1:
- return False
- try:
- playing = False
- try:
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- playing = t in (VCTurnState.TtsReceiving, VCTurnState.Playing)
- except Exception:
- playing = False
- if playing or self._vc_tts_pipeline_busy():
- self._ptt_short_press_stop = 1
- try:
- self._ptt_hard_mute_local_playback()
- except Exception:
- pass
- try:
- self.VC_Play_End()
- except Exception:
- pass
- try:
- self.fsm.await_played = False
- except Exception:
- pass
- try:
- self._ptt_led_lock_listening = 0
- self._ptt_interrupt_active = 0
- self.is_ruppt_tts = 0
- except Exception:
- pass
- try:
- self._vc_publish_led("idle", "ptt_short_press/stop_tts")
- except Exception:
- pass
- return True
- except Exception:
- pass
- return True
-
- def _ai_key_press_long_dialogue_interrupt(self):
- """长对话:TTS/播放忙或 turn==Thinking 时短按 → RequestToSpeak(先 cancel,canceled 后再开麦)。"""
- if self.VC_mode != 0:
- return False
- try:
- if getattr(self, "AI_client_VC_isconnect", 0) != 1 or getattr(self, "_vc_session_started", 0) != 1:
- return False
- except Exception:
- return False
- need_cancel = False
- if self._vc_tts_pipeline_busy():
- need_cancel = True
- else:
- try:
- if getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking:
- need_cancel = True
- except Exception:
- pass
- if not need_cancel:
- return False
- print("[KEY] long interrupt -> RequestToSpeak (cancel then uplink)")
- self.is_ruppt_tts = 1
- try:
- self._vc_pending_interrupt_uplink = 1
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})
- return True
-
- def _ai_key_press_long_dialogue_wake(self):
- """长对话未连接或未 started:直接 AI_VC_start,勿 Close+sleep。"""
- if self.VC_mode != 0:
- return False
- if getattr(self, "voice_flag", 0) == 1 or getattr(self, "meet_flag", 0) == 1 or getattr(self, "trans_flag", 0) == 1:
- return False
- try:
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
- return False
- except Exception:
- pass
- print("[KEY] long wake AI_VC_start (no Close)")
- try:
- self.Close_flag = 0
- except Exception:
- pass
- self.get_config_timer_count = 0
- self.key_state = 1
- self.AI_TTS_str = ""
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- self.AI_VC_start()
- self.AI_key_long_press_flag = 0
- return True
-
- def _vc_led_alert_rb_flash(self):
- try:
- EventMesh.publish("set_led_state", {"alert": "rb_flash_2"})
- except Exception:
- pass
-
- def _vc_clear_stale_record_state(self, reason=""):
- """record_state_flag 非 0 但可能无真实录音时先停录复位,避免 uplink 门控拒开麦。"""
- try:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- self.VC_Speech_End()
- except Exception:
- try:
- self.VC_record_state_flag = 0
- except Exception:
- pass
-
- def _ai_key_press_long_dialogue_when_session_ready(self):
- """长对话已连接且 session 已 started:非播放态直接开麦续聊(勿 idle 时发 cancel)。"""
- if self.VC_mode != 0:
- return False
- try:
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
- # 模式切换提示音会置 Close_flag=1;用户再短按须恢复可开麦
- try:
- self.Close_flag = 0
- except Exception:
- pass
- self._vc_clear_stale_record_state("key.press/continue")
- self.request_uplink_start("key.press/continue")
- return True
- except Exception:
- pass
- return False
-
- def _ai_key_release_handle_ptt(self):
- """单对话 PTT 松手:仅在上行录音中时 end_speech;禁止误触发 AI_VC_start(否则新 sid / cancel 竞态)。"""
- if self.AI_key_long_press_flag == 1:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- try:
- gap = 200
- if "vc_config" in globals() and vc_config:
- gap = int(vc_config.get_int("PTT_RELEASE_END_SPEECH_DELAY_MS", 200))
- if gap > 0:
- utime.sleep_ms(gap)
- except Exception:
- pass
- print("[PTT] release end_speech asr_str={!r} rec={}".format(
- self.AI_ASR_str, getattr(self, "VC_record_state_flag", -1)))
- self.AI_client_VC_end_speech()
- else:
- self.AI_key_long_press_flag = 0
- try:
- if getattr(self, "_ptt_short_press_stop", 0) == 1:
- self._ptt_short_press_stop = 0
- except Exception:
- pass
- print("[PTT] release_no_uplink_skip")
- return
- try:
- if getattr(self, "_ptt_short_press_stop", 0) == 1:
- self._ptt_short_press_stop = 0
- except Exception:
- pass
- print("[PTT] release_short_no_long")
-
- def _ai_key_long_press_route(self, event=None, msg=None):
- """长按:翻译先 Close 再开翻译录音;单对话→ptt_begin;长对话→与短按一致走 AI_key_press 链路。"""
- if self.trans_flag == 1:
- self.Close()
- EventMesh.publish("start_voice_record", 4)
- self.AI_key_long_press_flag = 1
- if self.VC_mode == 1:
- EventMesh.publish("publish_AI_btnInfo", {"func": self.ptt_begin})
- else:
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})
-
- # ===== Push-To-Talk(单对话模式)=====
- # 语义:
- # - 长按:无论当前处于 TTS/Thinking/任何中间态,立即打断并进入新一轮录音上行
- # - 松开:停止录音并等待服务端返回(由 AI_client_VC_end_speech 触发)
- def _ptt_end_led_suppress(self):
- # 保持 _vc_play_end_suppress_idle_led / _ptt_led_lock_listening 直至真开录(VC_Speech_Start)
- pass
-
- def _ptt_should_drop_inbound_tts(self):
- """单对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。"""
- try:
- if getattr(self, "VC_mode", -1) != 1:
- return False
- if getattr(self, "_drop_tts_audio", 0) == 1:
- return True
- if getattr(self, "is_ruppt_tts", 0) == 1:
- return True
- if getattr(self, "_ptt_interrupt_active", 0) == 1:
- return True
- if getattr(self, "_vc_serial_canceling", 0) == 1:
- return True
- if getattr(self, "_ptt_pending_arm", 0) == 1:
- return True
- except Exception:
- pass
- return False
-
- def _ptt_should_skip_remote_playback(self):
- return self._ptt_should_drop_inbound_tts()
-
- def _ptt_enter_interrupt_arm(self):
- try:
- self._ptt_arm_epoch = int(getattr(self, "_ptt_arm_epoch", 0)) + 1
- self._ptt_interrupt_active = 1
- self._ptt_led_lock_listening = 1
- self._vc_play_end_suppress_idle_led = 1
- self._drop_tts_audio = 1
- self.is_ruppt_tts = 1
- except Exception:
- pass
-
- def _ptt_abort_interrupt_arm(self):
- try:
- self._ptt_interrupt_active = 0
- self._ptt_led_lock_listening = 0
- self._vc_play_end_suppress_idle_led = 0
- self._drop_tts_audio = 0
- self.is_ruppt_tts = 0
- except Exception:
- pass
-
- def _ptt_on_record_hardware_started(self):
- """真开录后:允许新 TTS、放开灯锁;旧 TTS 已在打断期丢弃。"""
- try:
- self._ptt_interrupt_active = 0
- self._ptt_led_lock_listening = 0
- self._vc_play_end_suppress_idle_led = 0
- self._drop_tts_audio = 0
- self.is_ruppt_tts = 0
- except Exception:
- pass
-
- def _ptt_hard_mute_local_playback(self):
- try:
- self.VC_Play_flag = 0
- if getattr(self, "VC_play_TYPE", None) == VC_play_TYPE_PCM:
- if getattr(self, "play_pcm", None) is not None:
- try:
- self.play_pcm.stream_stop()
- self.play_pcm.close()
- except Exception:
- pass
- self.play_pcm = None
- try:
- self.aud.stopPlayStream()
- self.aud.stopAll()
- except Exception:
- pass
- if getattr(self, "AI_audio_queue", None) is not None:
- try:
- if self.AI_audio_queue.empty() != True:
- self.AI_audio_queue.clear()
- except Exception:
- pass
- except Exception:
- pass
- try:
- gap = 0
- if "vc_config" in globals() and vc_config:
- gap = int(vc_config.get_int("PTT_POST_MUTE_MS", 30))
- if gap > 0:
- utime.sleep_ms(gap)
- except Exception:
- pass
-
- def battery_priority_mute_vc(self, event=None, msg=None):
- """低电/关机提示音:短暂停 VC TTS,避免被流式播放挡住。"""
- try:
- self._ptt_hard_mute_local_playback()
- 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
-
- def _vc_enqueue_btn(self, func, data=None):
- """统一经 publish_AI_btnInfo 入队,在 btnInfo_queue_thread 串行执行。"""
- try:
- if data is not None:
- EventMesh.publish("publish_AI_btnInfo", {"func": func, "data": data})
- else:
- EventMesh.publish("publish_AI_btnInfo", {"func": func})
- except Exception:
- pass
-
- def _vc_ptt_cancel_wait_ms(self):
- try:
- if "vc_config" in globals() and vc_config:
- return int(vc_config.get_int("PTT_CANCEL_WAIT_MS", 2500))
- except Exception:
- pass
- return 2500
-
- def _vc_ptt_need_interrupt_wait(self):
- """单对话:服务端仍在思考/收 TTS/播报时,开录前须先 cancel 并等 canceled。"""
- try:
- if getattr(self, "VC_mode", -1) != 1:
- return False
- turn = getattr(self.fsm, "turn", VCTurnState.Idle)
- if self._vc_tts_pipeline_busy():
- return True
- if turn in (
- VCTurnState.Thinking, VCTurnState.TtsReceiving,
- VCTurnState.Playing, VCTurnState.AwaitPlayedAck,
- ):
- return True
- except Exception:
- pass
- return False
-
- def _vc_begin_ptt_cancel_wait(self, reason="ptt_interrupt"):
- try:
- self._ptt_pending_arm = 1
- self._vc_serial_canceling = 1
- try:
- self._vc_cancel_deadline_ms = utime.ticks_add(
- utime.ticks_ms(), self._vc_ptt_cancel_wait_ms())
- except Exception:
- self._vc_cancel_deadline_ms = 0
- json_data = {"event": "cancel", "payload": {}}
- self.AI_client_VC_send(ujson.dumps(json_data))
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- self._drop_tts_audio = 1
- print("[VC][serial] cancel_wait reason={} turn={}".format(
- reason, getattr(getattr(self, "fsm", None), "turn", "?")))
- except Exception:
- pass
-
- def _vc_on_ptt_cancel_settled(self, via="canceled"):
- """btn 线程:cancel 已收尾,执行挂起的 PTT 开录。"""
- try:
- if not getattr(self, "_vc_serial_canceling", 0) and not getattr(self, "_ptt_pending_arm", 0):
- return
- self._vc_serial_canceling = 0
- self._vc_cancel_deadline_ms = 0
- if getattr(self, "_ptt_pending_arm", 0):
- self._ptt_pending_arm = 0
- print("[VC][serial] ptt_arm after {}".format(via))
- self._ptt_arm_uplink()
- except Exception:
- pass
-
- def _vc_tick_serial_cancel_timeout(self):
- if not getattr(self, "_vc_serial_canceling", 0):
- return
- try:
- dl = getattr(self, "_vc_cancel_deadline_ms", 0)
- if dl and utime.ticks_diff(utime.ticks_ms(), dl) >= 0:
- print("[VC][serial] cancel_wait timeout -> arm")
- self._vc_on_ptt_cancel_settled(via="timeout")
- except Exception:
- pass
-
- def _vc_fsm_dispatch_cb(self, data):
- """btn 线程:处理服务端 FSM 事件(单对话串行)。"""
- try:
- ev, jsload = data
- except Exception:
- return
- try:
- actions = self.fsm.on_server_event(ev, jsload)
- self._fsm_apply_actions(actions)
- except Exception as e:
- usys.print_exception(e)
- if ev == "canceled":
- self._vc_on_ptt_cancel_settled(via="canceled")
-
- def _vc_dispatch_server_event(self, ev, jsload_data=None):
- 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:
- try:
- self._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))
- except Exception:
- pass
-
- def _ptt_arm_uplink(self, t0=0):
- """btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""
- try:
- self.is_ruppt_tts = 0
- except Exception:
- pass
- try:
- self._ptt_audio_created = 0
- except Exception:
- pass
- try:
- self.AI_ASR_flag = 0
- except Exception:
- pass
- try:
- self.AI_client_VC_send_speech()
- except Exception:
- pass
- try:
- self.VC_Speech_Start_now()
- except Exception:
- pass
- try:
- self._vc_publish_led("listening", "ptt_begin/recording")
- except Exception:
- pass
- self._ptt_end_led_suppress()
- try:
- if t0:
- print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))
- except Exception:
- pass
-
- def ptt_begin(self, reason="key.long_press"):
- try:
- if self.VC_mode != 1:
- return
- except Exception:
- return
-
- # PTT 新一轮开始:清理模式切换/提示音退出标志
- try:
- self.Close_flag = 0
- self._ptt_await_server = 0
- except Exception:
- pass
-
- t0 = 0
- try:
- t0 = utime.ticks_ms()
- except Exception:
- t0 = 0
-
- self._ptt_enter_interrupt_arm()
- try:
- self._vc_publish_led("listening", "ptt_begin/pre")
- except Exception:
- pass
-
- # 本地先硬静音再 VC_init,避免 DMA/队列尾包漏播
- try:
- self._ptt_hard_mute_local_playback()
- except Exception:
- pass
- 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:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- self.VC_Speech_End()
- except Exception:
- pass
-
- # 已在等 canceled:只刷新「待开录」,勿重复 audio.create
- try:
- if getattr(self, "_vc_serial_canceling", 0):
- self._ptt_pending_arm = 1
- print("[PTT] cancel in progress, refresh pending arm")
- self._ptt_end_led_suppress()
- return
- except Exception:
- pass
-
- # 会话未连接:先建链并发 Start,等待 started 后再自动开麦
- try:
- if getattr(self, "AI_client_VC_isconnect", 0) != 1:
- self._ptt_force_record_on_started = 1
- self.AI_VC_start()
- return
- except Exception:
- self._ptt_force_record_on_started = 1
- try:
- self.AI_VC_start()
- except Exception:
- pass
- return
-
- # 会话还没 initialized:先发 Start,请求服务端进入 started
- if getattr(self, "_vc_session_started", 0) != 1:
- self._ptt_force_record_on_started = 1
- try:
- try:
- if t0:
- print("[PTT] wait server.started (ms={})".format(utime.ticks_ms() - t0))
- except Exception:
- pass
- self.AI_client_VC_start_requset()
- except Exception:
- pass
- return
-
- # 服务端仍在思考/TTS:先 cancel,在 btn 队列收到 canceled 后再开录(方案甲串行)
- if self._vc_ptt_need_interrupt_wait():
- self._vc_begin_ptt_cancel_wait("ptt_begin")
- return
-
- self._ptt_arm_uplink(t0)
-
- def _vc_effective_asr_text(self):
- try:
- t = (getattr(self, "AI_ASR_str", "") or "").strip()
- if t:
- return t
- return (getattr(self, "_last_asr_result_text", "") or "").strip()
- except Exception:
- return ""
-
- def _vc_long_dialogue_auto_uplink_ok(self, reason):
- """长对话 canceled/completed 自动开麦:须有有效 ASR,且防抖/勿在 LLM 无 TTS 后抢开。"""
- try:
- if reason not in ("canceled", "completed", "local.play_end", "audio.played"):
- return True
- if getattr(self, "Close_flag", 0) == 1:
- return False
- if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
- pass
- elif not self._vc_effective_asr_text():
- try:
- print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason))
- except Exception:
- pass
- return False
- if reason == "completed":
- if getattr(self, "_turn_has_tts_started", 0) == 0:
- try:
- if getattr(self, "_llm_completed_ms", 0) or getattr(self.fsm, "turn", None) == VCTurnState.Thinking:
- print("[VC][uplink_ctrl] Deny START by completed cause=llm_without_tts")
- return False
- except Exception:
- pass
- try:
- now = utime.ticks_ms()
- last = getattr(self, "_vc_last_auto_uplink_ms", 0)
- if last and utime.ticks_diff(now, last) < 1200:
- print("[VC][uplink_ctrl] Deny START by {} cause=auto_uplink_debounce".format(reason))
- return False
- self._vc_last_auto_uplink_ms = now
- except Exception:
- pass
- except Exception:
- pass
- return True
-
- def _vc_led_state_debug_enabled(self):
- try:
- if "vc_config" in globals() and vc_config:
- return vc_config.get_bool("LED_STATE_DEBUG", True)
- except Exception:
- pass
- return True
-
- def _vc_led_should_standby_idle(self):
- """未录音且非 TTS 播中:待机/提示音/模式切换/单对话轮间应灭灯(DES-L3 idle),勿被 FSM.turn=Listening 误导。"""
- try:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- return False
- if self._vc_tts_playback_active():
- return False
- if getattr(self, "_ptt_await_server", 0) == 1:
- return False
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- if t in (
- VCTurnState.Thinking,
- VCTurnState.TtsReceiving,
- VCTurnState.Playing,
- VCTurnState.AwaitPlayedAck,
- ):
- return False
- if getattr(self, "Close_flag", 0) == 1:
- return True
- if getattr(self, "VC_mode", -1) == 1:
- return True
- k = getattr(self, "_last_tts_kind", "") or ""
- if k in ("prompt", "boot_prompt", "mode_switch_long", "mode_switch_ptt"):
- return True
- except Exception:
- pass
- return False
-
- def _vc_publish_led(self, ai_state, reason=""):
- try:
- if getattr(self, "_ptt_led_lock_listening", 0) == 1:
- if ai_state != "listening":
- if self._vc_led_state_debug_enabled():
- print("[LED] publish blocked ai={} reason={} (ptt_arm_lock)".format(
- ai_state, reason))
- return
- except Exception:
- pass
- try:
- if self._vc_led_state_debug_enabled():
- print("[LED] publish ai={} reason={} VC_mode={} rec={} turn={}".format(
- ai_state, reason, getattr(self, "VC_mode", -1),
- getattr(self, "VC_record_state_flag", -1),
- getattr(getattr(self, "fsm", None), "turn", "?")))
- except Exception:
- pass
- try:
- EventMesh.publish("set_led_state", {"ai_state": ai_state})
- except Exception:
- pass
-
- def _vc_led_sync_from_fsm(self):
- """按 FSM/真实 TTS 播放恢复 AI 灯;本地 aud.play 短音不参与。"""
- try:
- if getattr(self, "_ptt_led_lock_listening", 0) == 1:
- if getattr(self, "VC_record_state_flag", 0) == 0:
- return
- except Exception:
- pass
- try:
- if self._vc_led_should_standby_idle():
- self._vc_publish_led("idle", "fsm_sync/standby_idle")
- return
- except Exception:
- pass
- try:
- if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
- self._vc_publish_led("thinking", "fsm_sync/ptt_await")
- return
- except Exception:
- pass
- try:
- if self._vc_tts_playback_active():
- self._vc_publish_led("playing", "fsm_sync/tts")
- return
- except Exception:
- pass
- try:
- t = getattr(self.fsm, "turn", VCTurnState.Idle)
- if t in (VCTurnState.Thinking, VCTurnState.TtsReceiving):
- self._vc_publish_led("thinking", "fsm_sync/thinking")
- elif t in (VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
- # 服务端已标记播放态但本地 PCM 尚未起播:仍显示思考,勿误绿灯
- self._vc_publish_led("thinking", "fsm_sync/await_tts")
- elif t in (VCTurnState.Listening, VCTurnState.SpeechEnded, VCTurnState.SpeechStreaming):
- self._vc_publish_led("listening", "fsm_sync/listening")
- else:
- self._vc_publish_led("idle", "fsm_sync/idle")
- except Exception:
- try:
- self._vc_publish_led("idle", "fsm_sync/fallback")
- except Exception:
- pass
-
- def _uplink_request_start(self, reason="", *args, **kwargs):
- # 统一入口:避免多处同时 start 造成冲突(尤其是 TTS/播放阶段误开麦导致回声触发与状态紊乱)
- try:
- if self.Close_flag == 1:
- try:
- if reason in ("canceled", "local.play_end", "audio.played", "key.press/continue"):
- print("[VC][uplink_ctrl] Deny START by {} cause=Close_flag".format(reason))
- except Exception:
- pass
- return
- except Exception:
- return
-
- # 连续对话才允许“自动开新一轮上行”
- try:
- if self.VC_mode != 0:
- try:
- if reason in ("canceled", "local.play_end", "audio.played"):
- print("[VC][uplink_ctrl] Deny START by {} cause=VC_mode({})".format(reason, self.VC_mode))
- except Exception:
- pass
- return
- except Exception:
- return
-
- try:
- if self.VC_mode == 0 and not self._vc_long_dialogue_auto_uplink_ok(reason):
- return
- except Exception:
- pass
-
- try:
- if not self._vc_capture_allowed(reason):
- try:
- print("[VC][uplink_ctrl] Deny START by {} cause=playback_busy".format(reason))
- except Exception:
- pass
- return
- except Exception:
- return
-
- # ===== 业务门控(收敛到 FSM turn/session)=====
- try:
- if getattr(self.fsm, "session", VCSessionState.Disconnected) != VCSessionState.Started:
- try:
- if reason in ("canceled", "local.play_end", "audio.played"):
- print("[VC][uplink_ctrl] Deny START by {} cause=fsm.session".format(reason))
- except Exception:
- pass
- return
- turn = getattr(self.fsm, "turn", VCTurnState.Idle)
- # 仅 Listening/SpeechEnded 允许开麦进入下一轮;其它阶段一律拒绝(避免抢跑打乱链路)
- if turn not in (VCTurnState.Listening, VCTurnState.SpeechEnded):
- try:
- if reason in ("canceled", "local.play_end", "audio.played"):
- print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn))
- except Exception:
- pass
- return
- except Exception:
- return
-
- # 若当前不在录音上行,才允许开新一轮
- if getattr(self, "VC_record_state_flag", 0) != 0:
- # canceled 打断后:可能是僵死 flag=2(started 未真开麦),先停录复位再开麦
- if reason == "canceled":
- try:
- self._vc_clear_stale_record_state("uplink_start/canceled")
- except Exception:
- pass
- if getattr(self, "VC_record_state_flag", 0) != 0:
- try:
- if reason in ("canceled", "local.play_end", "audio.played"):
- print("[VC][uplink_ctrl] Deny START by {} cause=VC_record_state_flag({})".format(
- reason, getattr(self, "VC_record_state_flag", 0)))
- except Exception:
- pass
- return
-
- # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)
- if getattr(self, "record_data_save_flag", 0) == 0:
- self._server_vad_silence = 0
-
- print("[VC][uplink_ctrl] Request START by {}".format(reason))
- try:
- if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
- self._vc_pending_interrupt_uplink = 0
- print("[VC][uplink_ctrl] interrupt_uplink armed -> start (canceled)")
- except Exception:
- pass
- self.VC_Speech_Start_now()
-
- def _uplink_request_stop(self, reason="", *args, **kwargs):
- # 统一入口:避免多处同时 stop 造成冲突
- if getattr(self, "record_data_save_flag", 0) == 0:
- self._server_vad_silence = 1
-
- if getattr(self, "VC_record_state_flag", 0) == 0:
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] _uplink_request_stop skip (record_state==0) reason={}".format(reason))
- except Exception:
- pass
- return
-
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] step=uplink_stop_exec reason={} VC_record_state_flag={}".format(
- reason, getattr(self, "VC_record_state_flag", -1)))
- except Exception:
- pass
- print("[VC][uplink_ctrl] Request STOP by {}".format(reason))
- self.VC_Speech_End()
-
- def _uplink_mark_start(self, first_len=0):
- try:
- if self._uplink_active == 1:
- return
- self._uplink_active = 1
- self._uplink_bytes = 0
- self._uplink_start_ms = utime.ticks_ms()
- print("[VC][uplink] START len={} {} VC_record_state_flag={}".format(
- first_len, self._vc_fsm_log_snip(), self.VC_record_state_flag))
- except Exception:
- pass
-
- def _uplink_mark_end(self, reason=""):
- try:
- if self._uplink_active != 1:
- return
- dur = utime.ticks_diff(utime.ticks_ms(), self._uplink_start_ms) if self._uplink_start_ms else -1
- print("[VC][uplink] END reason={} bytes={} dur_ms={} {} VC_record_state_flag={}".format(
- reason, self._uplink_bytes, dur, self._vc_fsm_log_snip(), self.VC_record_state_flag))
- except Exception:
- pass
- self._uplink_active = 0
- self._uplink_bytes = 0
- self._uplink_start_ms = 0
-
- # 最近一次 TTS 的语义来源(dialog/prompt/mode_switch_long/mode_switch_ptt/...)
- self._last_tts_kind = ""
- # 下一个“无 dialog_id 的提示音 TTS”在 tts.started 时采用该 kind
- self._pending_tts_kind = ""
-
- # ===== 状态机动作接口(骨架)=====
- # 说明:统一把“副作用动作”收敛到这些方法,便于后续把 recv/timer 的 if/flag 逻辑迁移到 VCStateMachine。
- def request_send_start(self):
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_start_requset})
-
- def request_send_audio_create(self):
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_send_speech})
-
- def request_send_audio_completed(self):
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_end_speech})
-
- def request_send_audio_played(self):
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_audio_played})
-
- def request_send_synthesis(self, text):
- try:
- self.AI_TTS_str = text
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
-
- def request_uplink_start(self, reason=""):
- EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_start, "data": reason})
-
- def request_uplink_stop(self, reason=""):
- EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_stop, "data": reason})
-
- def _fsm_apply_actions(self, actions):
- 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 "")
- elif a[0] == "uplink_start":
- self.request_uplink_start(a[1] if len(a) > 1 else "")
- elif a[0] == "play_start":
- if getattr(self, "VC_mode", -1) == 1:
- if self._ptt_should_skip_remote_playback():
- if self._vc_led_state_debug_enabled():
- print("[VC][serial] skip play_start (ptt_interrupt)")
- continue
- try:
- self.fsm.on_local_play_start()
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_Start})
- elif a[0] == "send_audio_played":
- self.request_send_audio_played()
- except Exception:
- pass
-
- # AI
-
- def _print_hw_diag(self):
- # 仅诊断使用:打印当前分支能看到的麦克/音频相关配置和运行时状态。
- # JSWM2 老分支没有 device_profile/_PROFILE,这里宽容处理
- try:
- profile = _PROFILE if isinstance(_PROFILE, dict) else {}
- except Exception:
- profile = {}
- dev_type_profile = profile.get("dev_type", None)
- flags = profile.get("flags", {})
- hardware = profile.get("hardware", {})
- product_cfg = profile.get("product_cfg", {})
- vc_cfg = {}
- try:
- vc_cfg = product_cfg.get("vc", {})
- except Exception:
- vc_cfg = {}
-
- print("[hw-diag] ===== AI_VC hardware diag =====")
- print("[hw-diag] profile.dev_type =", dev_type_profile)
- print("[hw-diag] profile.flags =", flags)
- print("[hw-diag] profile.hardware =", hardware)
- print("[hw-diag] product_cfg.vc =", vc_cfg)
-
- try:
- print("[hw-diag] VC_record_samplerate =", self.VC_record_samplerate)
- print("[hw-diag] VC_play_samplerate =", self.VC_play_samplerate)
- except Exception:
- pass
- try:
- print("[hw-diag] vad_silence_runtime =", self.vad_silence)
- except Exception:
- pass
- try:
- print("[hw-diag] record_data_save_flag =", self.record_data_save_flag)
- print("[hw-diag] record_max_bytes =", self.record_max_bytes)
- except Exception:
- pass
-
- try:
- g = self.record.gain_get()
- except Exception as e:
- g = "gain_get_error: {}".format(e)
- print("[hw-diag] record.gain_get =", g)
- try:
- print("[hw-diag] type(record) =", type(self.record))
- print("[hw-diag] type(aud) =", type(self.aud))
- except Exception:
- pass
- print("[hw-diag] ===== end of hw-diag =====")
-
- def __init__(self):
- # import voiceCall
- # voiceCall.callStart("10086")
- # return
- self.http_get_config_flag = 0
- if self.http_get_config_flag == 1:
- try:
- self.http_get_config()#获取/更新配置参数
- except Exception as e:
- usys.print_exception(e)
- self.VC_30S_timer = osTimer()
- self.ai_key_press_audio = osTimer()
- self.VC_30S_timer.start(30000, 1, self.VC_30S_timer_cb)
- self.online_kws = 0#是否开启线上关键词
- self.get_config_timer_count = 0
- self.record_data_save_flag = 0 #是否保存录音文件
- self.play_data_save_flag = 0#是否保存tts数据到文件
- self.record_fd = None
- self.play_fd = None
- self.is_VC_ACK = 0 #是否开启ack应答模式
- self.is_long_record = 0 #是否开启长录音模式(语音打断模式)
- self.ai_OTA = OTA()
- self.is_auth = 1 #是否开启dmp鉴权
- #self.VC_mode = 1 #0:短按连续问答模式 1:长按录音模式
- self.VC_mode = EventMesh.publish("get_storage_config","VC_mode")#
- if self.VC_mode == None :
- self.VC_mode = 0
- self.is_up_error= 0 #是否上传错误id
- self.Close_flag =0 #=1时,连续对话播放完音频就退出
- # 服务端 VAD 结束标志:收到 vad.silence 后置 1,用于硬闸门禁止继续上行音频
- # (避免仍持续上行导致服务端推迟/抑制后续 TTS,从而一直停留在思考态蓝绿闪)
- self._server_vad_silence = 0
- # 单轮状态标志:用于判定 completed 是否应自动进入下一轮
- self._turn_has_asr_completed = 0
- self._turn_has_vad_silence = 0
- self._turn_has_tts_started = 0
- # VC 会话是否已进入 started(服务端 session initialized)
- # 重要:在收到 started 前发送音频帧会触发服务端 "session not initialized" → failed → 端侧重连死循环
- self._vc_session_started = 0
- # 开机/提示音:本轮 started 不自动开麦(避免问候 TTS 前误进 listening)
- self._skip_started_uplink_once = 0
- self._last_tts_kind = ""
- self._pending_tts_kind = ""
- # Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create
- # 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed
- self._ptt_audio_created = 0
- # PTT:松手等服务器期间保持 thinking 勿回蓝闪聆听
- self._ptt_await_server = 0
- # 方案甲 btnInfo 串行:打断时先 cancel,收到 canceled(或超时)后再 audio.create
- self._vc_serial_canceling = 0
- self._ptt_pending_arm = 0
- self._vc_cancel_deadline_ms = 0
- # PTT 打断:轮次号 + 开录前丢弃旧 TTS / 灯只显示聆听
- self._ptt_arm_epoch = 0
- self._ptt_interrupt_active = 0
- self._ptt_led_lock_listening = 0
- # 端侧主动发 synthesis 后到收到 tts.started 之间的窗口标志,用于阻止误开下一轮录音上行
- self._tts_pending = 0
- self._tts_pending_ms = 0
- # 各类兜底的节流时间戳(避免“兜底过激/连续触发”)
- self._fb_last_thinking_restart_ms = 0
- self._fb_last_tts_pending_timeout_ms = 0
- # thinking 兜底:部分情况下服务端只返回 llm.completed 而不触发 tts.started/completed,
- # 会导致一直停留在 thinking。此时超时自动回到聆听。
- self._llm_completed_ms = 0
- # 语音上行统计(真实发送到 VC 发送队列的开始/结束)
- self._uplink_active = 0
- self._uplink_bytes = 0
- self._uplink_start_ms = 0
-
- # 状态机骨架(后续逐步把旧逻辑迁移进来)
- self.fsm = VCStateMachine(self)
-
- self.play_new_voice_flag =0 #播放文字留言flag
-
- self.wait_first_flag = 0#是否进入未读判定(只播报一次)
- self.wait_first_timer = osTimer()#如果播放未读留言的时候未读则不要播放了
- self.wait_first_cnt = 0
-
- self.wait_sort = 0#未读留言正在播第几条
- EventMesh.subscribe("play_tts",self.play_tts)
- EventMesh.subscribe("play_new_voice",self.play_new_voice)
- EventMesh.subscribe("play_his_voice",self.play_his_voice)
- EventMesh.subscribe("play_wait_voice",self.play_wait_voice)
- EventMesh.subscribe("play_single_voice",self.play_single_voice)#不带tts的播放(闹钟)
- EventMesh.subscribe("play_music",self.play_music)#播放音乐
- EventMesh.subscribe("play_trans_tts",self.play_trans_tts)#接收翻译tts
- EventMesh.subscribe("set_vad_silence",self.set_vad_silence)
- EventMesh.subscribe("set_wait_first_flag",self.set_wait_first_flag)
- EventMesh.subscribe("kws_even",self.kws_even)
- EventMesh.subscribe("change_volume",self.change_volume)
- EventMesh.subscribe("find_cmd",self.find_cmd) #查询命令
- EventMesh.subscribe("NLP_set_vad_silence",self.NLP_set_vad_silence) #设置聆听间隔
- EventMesh.subscribe("change_voice",self.change_voice) #改变音色
- EventMesh.subscribe("Close_VC",self.Close_VC) #外部结束对话
- EventMesh.subscribe("get_recv_voice",self.get_recv_voice) #获取留言播放状态
- EventMesh.subscribe("set_recv_voice",self.set_recv_voice) #设置留言播放状态
- self.voice_timer = osTimer()#语音留言10S按下按键确定是否播放的判断定时器
- self.voice_cnt = 0 #留言定时器计数
- self.voicerupt_flag = 0#留言选择按键接听标志位
- self.wait_voicerupt_flag = 0#未读留言选择按键接听标志位
- self.recv_voice = 0 #是否收到留言
- self.voice_notice = 0 #收到留言并播放则置为1,接下来要播放提示词
- self.voice_choose_flag = 0#当tts和语音留言冲突时,为了防止语音地址被误清,做标志位在close做判断
-
- self.revoice_timer = osTimer()#语音留言10S按下按键确定是否回复的判断定时器
- self.revoice_cnt = 0 #回复留言定时器计数
- self.revoicerupt_flag = 0#留言选择按键接听标志位
-
- self.music_timer = osTimer()#音乐播放定时器,等待tts播完才放音乐
- self.music_cnt = 0 #留言定时器计数
-
- self.his_timer = osTimer()#历史留言播放定时器,等待tts播完再播留言
- self.his_cnt = 0#历史留言定时器技术
-
- self.wait_timer = osTimer()#未读留言播放定时器,等待tts播完再播留言
- self.wait_cnt = 0#未读留言定时器技术
-
- self.error_data = []
- self.asr_taskid = ""
- self.VC_taskid = ""
- self.VC_server = 2 #0柒牌正式服务器,1:测试服务器1,2:测试服务器2
- self.voice_flag = 0
-
- #会议记录标志
- self.meet_flag = 0
- #AI翻译标志
- self.trans_flag = 0
-
- #历史留言查看判断标志
- self.history_voice_flag = 0#公共变量判断
- #60S等待留言判断标志
- self.wait_voice_flag = 0#公共变量判断
- EventMesh.publish("AI_alarm_open")#
- self.server_ip = EventMesh.publish("get_storage_config","server_ip")#
- if self.server_ip == None:
- if self.VC_server == 1:#
- self.server_ip="118.145.158.207:22083"
- elif self.VC_server == 2:#
- self.server_ip="118.145.158.207:22082"
- elif self.VC_server == 4:#
- self.server_ip="118.145.158.207:22084"
- elif self.VC_server == 5:#
- self.server_ip="amp-wss.raisound.com"
- elif self.VC_server == 6:#
- self.server_ip="118.145.158.207:22080"
- elif self.VC_server == 7:#
- self.server_ip="118.145.158.207:22080"
- elif self.VC_server == 8:#
- self.server_ip="115.190.94.208:22080"
- elif self.VC_server == 86:#
- self.server_ip="118.145.158.207:22086"
- else:
- #self.server_ip="sevenpin.qipai.com.cn"
- self.server_ip="118.145.158.207:22082"
- # 默认走 constants.AMP_SERVER(由 default_srv.py 统一管理,可在 usr 覆盖)
- try:
- if constants.AMP_SERVER:
- self.server_ip = constants.AMP_SERVER
- except Exception:
- pass
-
- #开机提示音
- self.power_on_tone = EventMesh.publish("get_storage_config","power_on_tone")#
- try:
- if "vc_config" in globals() and vc_config:
- if not vc_config.get_bool("POWER_ON_TONE_ENABLED", True):
- self.power_on_tone = None
- except Exception:
- pass
- if self.power_on_tone == None:
- try:
- if "vc_config" in globals() and vc_config:
- self.power_on_tone = vc_config.get_attr("POWER_ON_TONE_DEFAULT", None)
- except Exception:
- pass
-
- self.VC_record_TYPE = VC_record_TYPE_PCM #0:PCM 1:AMR
- self.VC_play_TYPE = VC_play_TYPE_PCM #0:PCM 1:mp3
- self.VC_record_samplerate = 16000
- self.VC_play_samplerate = 16000
- self.VAD_data_len = 640 #10ms数据
- self.is_VAD = 0#=1打开vad打断
- # ARG_sample_rate, // 采样率
- # ARG_channel, // 通道数
- # ARG_frame_size, // 每帧字节数
- # ARG_energy_threshold, // 能量阈值
- # ARG_voice_time, // 语音持续时间阈值(ms)
- # ARG_silence_time, // 静音持续时间阈值(ms)
- # ARG_silence_start_threshold, // 静音开始阈值(ms)
- self.vad = None
- if self.is_VAD == 1:
- self.vad = VAD.VAD(self.VC_record_samplerate ,1,self.VAD_data_len,600,100,300,100)
- self.speech_rate = EventMesh.publish("get_storage_config","speech_rate")#语速配置
- if self.speech_rate == None:
- self.speech_rate = 0
-
- self.vad_silence = EventMesh.publish("get_storage_config","vad_silence")#语速配置
- if self.vad_silence == None:
- try:
- if "vc_config" in globals() and vc_config:
- self.vad_silence = vc_config.get_int("VAD_SILENCE_DEFAULT", 300)
- else:
- self.vad_silence = 300
- except Exception:
- self.vad_silence = 300
-
- self.VC_TTS_voice_type = EventMesh.publish("get_storage_config","VC_TTS_voice_type")#音色配置
- if self.VC_TTS_voice_type == None :
- try:
- if "vc_config" in globals() and vc_config:
- cfg = vc_config.get_attr("VC_TTS_VOICE_TYPE_DEFAULT", None)
- if isinstance(cfg, dict):
- self.VC_TTS_voice_type = cfg
- except Exception:
- pass
- # 若配置未提供,沿用原逻辑内置兜底(保持兼容)
- if self.VC_TTS_voice_type == None:
- self.VC_TTS_voice_type = {
- "女":{"bot_name":"柒妹妹","voice":"zh_female_wanwanxiaohe_moon_bigtts","lang":"zh-cn"},
- "男":{"bot_name":"柒先生","voice":"zh_male_wennuanahu_moon_bigtts","lang":"zh-cn"},
- "童":{"bot_name":"柒乐乐","voice":"ICL_zh_female_huoponvhai_tob","lang":"zh-cn"},
- }
-
- # self.VC_TTS_voice_type = {0:"zh_female_xinlingjitang_moon_bigtts",1:"zh_male_wennuanahu_moon_bigtts",2:"ICL_zh_female_huoponvhai_tob",
- # 3:"aliyun::longjiaxin",4:"zh_female_daimengchuanmei_moon_bigtts"}
- #self.VC_TTS_voice_type = ["aliyun::longxiaoxia","aliyun::longnan","aliyun::longling"]
-
- #EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])
-
- try:
- self.VC_TTS_voice_key = EventMesh.publish("get_storage_config","VC_TTS_voice_key")#音色配置
- if self.VC_TTS_voice_key == None :
- try:
- if "vc_config" in globals() and vc_config:
- self.VC_TTS_voice_key = vc_config.get_attr("VC_TTS_VOICE_KEY_DEFAULT", None)
- except Exception:
- pass
- if not self.VC_TTS_voice_key:
- self.VC_TTS_voice_key = "男"
- if self.VC_TTS_voice_key in self.VC_TTS_voice_type:
- pass
- else:
- self.VC_TTS_voice_key = list(self.VC_TTS_voice_type.keys())[0]
- print("音色配置:{}:{}\r\n\r\n".format(self.VC_TTS_voice_key,self.VC_TTS_voice_type[self.VC_TTS_voice_key]))
- self.VC_TTS_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置
- except Exception as e:
- usys.print_exception(e)
- #配置词
- self.commands_mode = 2 #=1用大模型判断命令,=0用服务器本地插件判断命令 =2用设备本地判断方式
- self.commands = []
- if self.VC_TTS_volume == None:
- self.VC_TTS_volume = 100
- #可ota配置的参数变量
- self.emotion = EventMesh.publish("get_storage_config","emotion")#情绪
- if self.emotion == None:
- self.emotion = ""
- self.ota_update_notice = EventMesh.publish("get_storage_config","ota_update_notice")#OTA升级提示语
- if self.ota_update_notice == None:
- self.ota_update_notice = ""
- self.AI_audio_queue_thread_id = 0
- self.AI_audio_queue_thread_lock = 0
- self.AI_VC_send_data_queue_thread_id = 0
- self.AI_VC_send_data_queue_thread_lock = 0
- self.AI_ASR_send_data_queue_thread_id = 0
- self.AI_ASR_send_data_queue_thread_lock = 0
- self.AI_record_tmp_queue = Queue(100)
- self.AI_ASR_send_data_queue = Queue(200)
- self.AI_VC_send_data_queue = Queue(100)
- self.AI_audio_queue = Queue(100)
- #self.AI_WS_connect_Queue = Queue(100) #释放用
- self.AI_TTS_str = ""
- self.AI_ASR_str = ""
- self.AI_ASR_flag = 0
- self.AI_ASR_str_tmp = ""
- self.AI_ASR_send_buf_len = 0
- self.AI_ASR_text_tmp = ""
- self.AI_ASR_text_repet_count = 0
- # #开始线程
- # if self.AI_audio_queue_thread_id ==0:
- # self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())
- # else:
- # print("AI_VC_start :AI_audio_queue_thread_id != 0 ")
- # if self.AI_VC_send_data_queue_thread_id == 0:
- # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())
-
- self.VC_dialog_id = ""#
- self.VC_message_id=None
- self.VC_task_id =None
- self.AI_client_VC = None
- self.AI_client_VC_recv_id = 0
- self.AI_client_VC_recv_lock = 0
- self.AI_client_VC_isconnect =0
- self.AI_client_VC_debug =0
-
- self.asr_state = 0 # 预留:ASR 侧状态(当前未参与分支)
- self.key_state = 0
-
- self.AI_client_asr = None
- self.AI_client_asr_recv_id = 0
- self.AI_client_asr_recv_lock = 0
- self.AI_client_asr_isconnect =0
- self.AI_ASR_timer = osTimer()
- self.AI_ASR_timer_count = 0
- self.AI_client_asr_debug =0
- self.AI_ASR_check_timeout = 0
- self.AI_ASR_error_timeout = 0
- self.VC_Thinking_timerout = 0
-
-
- self.record_pcm = None
- self.play_pcm = None
- self.VC_record_thread_id = 0
- self.VC_record_thread_lock = 0
- self.VC_pingping_timerout = 0
- self.asr_pingping_timerout = 0
- self.factory_timer = osTimer()
- self._1S_timer = osTimer()
- self.Audio_buf_size = 20*1024
- self.record = audio.Record()
- #self.record.end_callback(self.record_callback)
- self.record.gain_set(4,10)
- #self.record.gain_set(4,12)
- self.aud = audio.Audio(0)
- self.aud.setCallback(self.audio_cb)
- # 确保 record/aud 已创建后再打印硬件/音频诊断信息(需要 gain_get/type(record))
- try:
- self._print_hw_diag()
- except Exception as e:
- usys.print_exception(e)
- #self.aud.setVolume(self.VC_TTS_volume //10)
- self.audio_play_state = 1 #音频播放完状态
- self.close_timerout = 0
- self._idle_deep_sec = 0
- self._vc_idle_deep_armed_suppress = 0
- self.LocalRespondingEnded_timerout = 0
-
- self.VC_Play_flag = 0
- self.play_flag = 0
- self.VC_record_state_flag = 0
- self.AI_key_long_press_flag = 0
- self._asr_completed_tone_done = 0 # asr.completed 本地 think 提示每轮仅一次
- self._last_asr_result_text = "" # 保留最近 asr.result,防 VC_Speech_End 早于 asr.completed 清空
- self._vc_last_auto_uplink_ms = 0 # 长对话自动开麦防抖
- self._vc_local_play_active = 0 # aud.play 本地短音占用(与采集互斥)
- self._vc_local_play_until_ms = 0
- self._vc_pending_interrupt_uplink = 0 # 长对话短按打断:等 canceled 再开麦
- self.websocket_error_count = 0
- self.AI_new_voice_url = "" #未读语音地址/音乐播放地址/闹钟播放标志
- self.music_play_flag = 0 #音乐播放标志
- self.alarm_play_flag = 0 #闹钟播放标志
- self.tts_in_data_len = 0
- self.tts_out_data_len = 0
- self._need_audio_played_ack = 0#tts.completed 后需在本地播报结束后上报 audio.played;勿用 tts_in/out:tts.completed 会再调 VC_Play_Start 把二者清零
- self.VC_satrt_flag = 0
- self.VC_stop_play_audio_flag = 0
- self.VC_satrt_kws_flag = 0
- self.session_id = ""
- self.stop_voice_timer=osTimer()
- self.pwk_cnt_voice = 0 #不听语音后,积攒的次数,需要一起播放
- self.pwk_notice_flag = 0#是否处于积攒留言状态(用来实现单次提醒)
- self.is_noread_voice = EventMesh.publish("get_storage_config","is_noread_voice")
- if self.is_noread_voice == None:
- self.is_noread_voice = 0#是否还有未读留言(开机判断)
- self.is_ruppt_tts = 0#是否已经打断tts播放
- 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)
- EventMesh.subscribe("AI_key_release", self.AI_key_release)
- EventMesh.subscribe("pwk_key_double_press", self.pwk_key_double_press)
- EventMesh.subscribe("AI_key_long_press", self.AI_key_long_press)
- EventMesh.subscribe("pwk_key_release", self.pwk_key_release)
- EventMesh.subscribe("get_AI_audio_queue", self.get_AI_audio_queue)
- EventMesh.subscribe("get_VC_satrt_flag", self.get_VC_satrt_flag)
- EventMesh.subscribe("get_vc_stream_active", self.get_vc_stream_active)
- EventMesh.subscribe("get_vc_tts_playback_active", self.get_vc_tts_playback_active)
- EventMesh.subscribe("battery_priority_mute_vc", self.battery_priority_mute_vc)
- EventMesh.subscribe("VC_error", self.VC_error)
- EventMesh.subscribe("audio_test", self.audio_test)
- EventMesh.subscribe("audio_test_close", self.audio_test_close)
- EventMesh.subscribe("set_record_data_save_flag", self.set_record_data_save_flag)
- EventMesh.subscribe("set_VC_TTS_voice_type", self.set_VC_TTS_voice_type)
- EventMesh.subscribe("SET_TTS_VOLUME", self.set_tts_volume)
- EventMesh.subscribe("set_VC_stop_play_audio_flag", self.set_VC_stop_play_audio_flag)
- EventMesh.subscribe("set_yuyinaudio_flag",self.set_yuyinaudio_flag)
- EventMesh.subscribe("get_yuyinaudio_flag",self.get_yuyinaudio_flag)
- EventMesh.subscribe("set_pwk_cnt_voice",self.set_pwk_cnt_voice)
- EventMesh.subscribe("get_pwk_cnt_voice",self.get_pwk_cnt_voice)
- EventMesh.subscribe("exit_voice_message_mode",self.exit_voice_message_mode) #超时未退出语音留言模式
- EventMesh.subscribe("come_meet_mode",self.come_meet_mode)#进入会议模式
- EventMesh.subscribe("exit_meet_mode",self.exit_meet_mode)#退出会议模式
- EventMesh.subscribe("come_trans_mode",self.come_trans_mode)#进入翻译模式
- EventMesh.subscribe("exit_trans_mode",self.exit_trans_mode)#退出翻译模式
- EventMesh.subscribe("set_trans_flag",self.set_trans_flag)#设置翻译状态
- EventMesh.subscribe("get_audio_play_state",self.get_audio_play_state)
- EventMesh.subscribe("set_voicerupt_flag",self.set_voicerupt_flag)#按键中断触发留言播放
- EventMesh.subscribe("set_wait_voicerupt_flag",self.set_wait_voicerupt_flag)#按键中断触发未读留言播放
- EventMesh.subscribe("get_recv_voice",self.get_recv_voice)#获取是否收到留言
- EventMesh.subscribe("rate_change",self.rate_change)#直接修改语速
- EventMesh.subscribe("get_history_voice_flag",self.get_history_voice_flag)#获取查找历史留言状态
- EventMesh.subscribe("set_history_voice_flag",self.set_history_voice_flag)#修改查找历史留言状态
- EventMesh.subscribe("clean_unread_vocie",self.clean_unread_vocie)#修改查找历史留言状态
- EventMesh.subscribe("get_wait_voice_flag",self.get_wait_voice_flag)#获取60S等待留言播放定时器状态
- EventMesh.subscribe("set_wait_voice_flag",self.set_wait_voice_flag)#修改60S等待留言播放定时器状态
- EventMesh.subscribe("rate_move",self.rate_move)#NLP修改语速
- EventMesh.subscribe("find_rate",self.find_rate)#NLP查询语速
- EventMesh.subscribe("set_pwk_notice_flag",self.set_pwk_notice_flag)#设置未读状态
- # EventMesh.subscribe("get_voicerupt_flag",self.get_voicerupt_flag)#获取留言播放状态
- EventMesh.subscribe("chenggong_tone",self.chenggong_tone)
-
- pass
- EventMesh.publish("set_led_state", {"ai_state": "idle"})
- self.dialog_attributes_provider = EventMesh.publish("get_storage_config","dialog_attributes_provider")#智能体类型配置
- if self.dialog_attributes_provider == None:
- self.dialog_attributes_provider = "coze"
- self.outbound_attributess_provider = EventMesh.publish("get_storage_config","outbound_attributess_provider")#tts服务选择
- if self.outbound_attributess_provider == None:
- self.outbound_attributess_provider = "volcengine"
- self.inbound_attributess_provider = EventMesh.publish("get_storage_config","inbound_attributess_provider")#tts服务选择
- if self.inbound_attributess_provider == None:
- self.inbound_attributess_provider = "volcengine"
-
- self.bot_name = "柒先生"
- try:
- if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:
- self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]
- else:
- print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")
- except Exception as e:
- usys.print_exception(e)
- try:
- bt_ota_flag=0
- try:
- if "vc_config" in globals() and vc_config and vc_config.get_bool("BT_OTA_ENABLED", False):
- bt_ota_flag = self.ai_OTA.bt_ota()#先检查蓝牙是否要升级
- except Exception:
- pass
-
- cat1_ota_flag = self.ai_OTA.min_sys_ota()
- if bt_ota_flag==1 or cat1_ota_flag == 1:
- #if 0:
- if self.ota_update_notice != "":
- self.AI_TTS_str = self.ota_update_notice
- else:
- self.AI_TTS_str = "检测到有新版本需要升级啦!升级时可千万别断电或乱按哦,升级完成后设备会自动重启,重启就代表升级成功"
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- self.Close_flag = 1
- # if self.ai_OTA.min_sys_ota()==2:
- # self.AI_TTS_str = "检测到有新版本需要升级啦!电量低于40%,无法保证升级正常执行,请充满电后重启设备再次升级!"
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- else:
- try:
- self.ai_OTA.ota_result()
- self.ai_OTA.pots_ICCID()
- except Exception as e:
- usys.print_exception(e)
- #开机提示语,如果联网失败,直接提示信号弱
-
-
- #EventMesh.publish("get_storage_config",["powerRestart",1])
- powerRestart = EventMesh.publish("get_storage_config","powerRestart")#=1非正常重启
- if powerRestart == None:
- powerRestart = 0
- if powerRestart == 1:
- EventMesh.publish("set_storage_config",["powerRestart",0])
-
- else:
- pass
- self.AI_TTS_str = self.power_on_tone.replace("bot_name", self.bot_name)
- print("开机提示:{}".format(self.AI_TTS_str))
- try:
- self._pending_tts_kind = "boot_prompt"
- self._last_tts_kind = "boot_prompt"
- self._skip_started_uplink_once = 1
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
-
- except Exception as e:
- usys.print_exception(e)
- def set_VC_stop_play_audio_flag(self,event = None, msg = None):
- self.VC_stop_play_audio_flag = msg
- print("self.VC_stop_play_audio_flag:{}".format(self.VC_stop_play_audio_flag))
-
- def rate_change(self,event,msg):#直接设置语速(数字)
- self.speech_rate = msg
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
- print("set_speech_rate:{}".format(self.speech_rate))
- def rate_move(self,event,msg):#设置语速(模糊(快/慢一点))
- if msg == 1 and self.speech_rate<50:#增大
- self.speech_rate += 10
- else:#减小
- if self.speech_rate >-50:
- self.speech_rate -= 10
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
- print("set_speech_rate:{}".format(self.speech_rate))
- def find_rate(self,event,msg):#查询APP语速
- print("查询语速处理:原始数据为:{}".format(self.speech_rate))
- y = 1
- if self.speech_rate >= -50 and self.speech_rate < 0:#负数
- y = 1 - (-0.01 * self.speech_rate)
- elif self.speech_rate <= 50 and self.speech_rate >= 0:#正数
- y = 0.02 * self.speech_rate + 1.0
- print("处理后数据为{}".format(y))
- rate = str(y) + "倍"
- return rate
- def set_pwk_notice_flag(self,event,msg):#设置未读状态
- self.pwk_notice_flag = msg
- #获取60S等待留言播放定时器状态
- def get_wait_voice_flag(self,event,msg):
- return self.wait_voice_flag
- def set_wait_voice_flag(self,event,msg):
- # print("未读留言标记修改为:{}".format(msg))
- self.wait_voice_flag = msg
-
- #查看历史留言状态修改
- def get_history_voice_flag(self,event,msg):
- return self.history_voice_flag
- def set_history_voice_flag(self,event,msg):
- # print("历史留言标记修改为:{}".format(msg))
- self.history_voice_flag = msg
- def clean_unread_vocie(self,event,msg):
- self.pwk_cnt_voice = 0
- self.pwk_notice_flag = 0
- EventMesh.publish("set_app_new_voice_flag",0)
- def audio_test(self,event = None, msg = None):
- self._uplink_request_start("audio_test")
- self.VC_Play_Start()
- def audio_test_close(self,event = None, msg = None):
- self._uplink_request_stop("audio_test_close")
- self.VC_Play_End()
- if self.record_data_save_flag == 1:
- if self.record_fd :
- self.record_fd.close()
- self.record_fd = None
- def set_record_data_save_flag(self,event = None, msg = None):
- self.record_data_save_flag = msg
- def post_processor_after_instantiation(self):
- pass
- def get_AI_audio_queue(self, event, msg):
- return self.AI_audio_queue
- def get_VC_satrt_flag(self, event, msg):
- return self.VC_satrt_flag
- #检查有没有意外打开ai部分功能
- def check_vc_start(self):
- print("check_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))
- if self.VC_satrt_flag == 0:
- if self.VC_record_state_flag != 0 or self.AI_client_VC_isconnect == 1 or self.VC_Play_flag == 1:
- self.Close()
- def VC_30S_timer_cb(self, *args): #
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_30S_timer_cb_})
-
- def VC_30S_timer_cb_(self):
- self.get_config_timer_count += 1
- if self.get_config_timer_count >= 2*60*2:#2小时更新配置参数
- self.get_config_timer_count = 0
- if self.http_get_config_flag == 1:
- try:
- self.http_get_config()#获取/更新配置参数
- except Exception as e:
- usys.print_exception(e)
- self.check_vc_start()#30秒检查一下有没有关闭
- #提交错误id
- def ud_error(self,error_data):
- response = None
- if self.is_up_error == 1:
- try:
- # currentVersion = versions.Build_Time
- # currentVersion = "7PIN-"+currentVersion[:10]+"-"+currentVersion[11:-3]
- url = "http://{}/health?task_id={}".format(self.server_ip,error_data[0]+error_data[1])
- print(url)
- response = request.get(url,timeout=5)
- if response.status_code == 200:
-
- print("ud_error:ok")
- else:
- print("ud_error error!!!!")
- for i in response.text:
- print(i)
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
- #获取配置参数
- def http_get_config(self, event=None, msg=None):
- print("http_get_config")
-
- response = None
- #http://180.184.206.142/bz-api/dev/config?deviceId=xxx
- try:
- url = (constants.DMP_BASE + "/dev/config/{}").format(modem.getDevImei())
-
-
- print(url)
- response = request.get(url,timeout=5)
- if response.status_code == 200:
- data = response.json()
- print("http_get_config:{}".format(data))
-
- if data:
- try:
- if data["code"] == 20000:#有配置
- if data["data"] :
- if "version" in data["data"]:
- config_version = EventMesh.publish("get_storage_config","config_version")#音量配置
- if config_version != data["data"]["version"]:
- config_version = data["data"]["version"]
- EventMesh.publish("set_storage_config",["config_version",config_version])
- if "VC_TTS_voice_type" in data["data"]:
- self.VC_TTS_voice_type = data["data"]["VC_TTS_voice_type"]
- EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])
- print("配置VC_TTS_voice_type成功")
- if "server_ip" in data["data"]:
- self.server_ip = data["data"]["server_ip"]
- EventMesh.publish("set_storage_config",["server_ip",self.server_ip])
- print("配置server_ip成功")
- if "power_on_tone" in data["data"]:
- self.power_on_tone = data["data"]["power_on_tone"]
- EventMesh.publish("set_storage_config",["power_on_tone",self.power_on_tone])
- print("配置power_on_tone成功")
- if "dialog_attributes_provider" in data["data"]:
- self.dialog_attributes_provider = data["data"]["dialog_attributes_provider"]
- EventMesh.publish("set_storage_config",["dialog_attributes_provider",self.dialog_attributes_provider])
- if "outbound_attributess_provider" in data["data"]:
- self.outbound_attributess_provider = data["data"]["outbound_attributess_provider"]
- EventMesh.publish("set_storage_config",["outbound_attributess_provider",self.outbound_attributess_provider])
- print("配置outbound_attributess_provider成功")
- if "inbound_attributess_provider" in data["data"]:
- self.inbound_attributess_provider = data["data"]["inbound_attributess_provider"]
- EventMesh.publish("set_storage_config",["inbound_attributess_provider",self.inbound_attributess_provider])
- print("配置inbound_attributess_provider成功")
- if "speech_rate" in data["data"]:
- self.speech_rate = EventMesh.publish("SET_SPEECH_RATE",float(data["data"]["speech_rate"]))
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
- print("配置speech_rate成功")
- if "emotion" in data["data"]:
- self.emotion = data["data"]["emotion"]
- EventMesh.publish("set_storage_config",["emotion",self.emotion])
- print("配置emotion成功")
- if "ota_update_notice" in data["data"]:
- self.ota_update_notice =data["data"]["ota_update_notice"]
- EventMesh.publish("set_storage_config",["ota_update_notice",self.ota_update_notice])
- print("配置ota_update_notice成功")
- if "VC_TTS_volume" in data["data"]:
- self.VC_TTS_volume =int(data["data"]["VC_TTS_volume"])
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- print("配置VC_TTS_volume成功")
-
- except Exception as e:
- usys.print_exception(e)
- else:
- print("status_code:{}".format(response.status_code))
- print("http_get_config:error!!!!!!!!!!")
- response.close()
- except Exception as e:
- usys.print_exception(e)
- if response != None:
- response.close()
-
- def audio_cb(self,event):
- if event == 0:
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- self.audio_play_state =1
- #EventMesh.publish("set_audio_state", 1)
- print('audio-play start.')
- # 本地 aud.play(开录/思考短音等)不改动 AI 灯,仅 TTS 流由 VC_Play_Start 控绿灯
- if self._vc_led_state_debug_enabled():
- print("[LED] audio_cb start skip (local_play)")
- return
- elif event == 7:
- self.audio_play_state = 0
- #EventMesh.publish("set_audio_state", 0)
- print('audio-play finish.!!!!!!')
- try:
- if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
- self._vc_publish_led("thinking", "audio_cb/finish/ptt_await")
- return
- except Exception:
- pass
- try:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- self._vc_publish_led("listening", "audio_cb/finish/recording")
- return
- except Exception:
- pass
- try:
- if self._vc_tts_playback_active():
- if self._vc_led_state_debug_enabled():
- print("[LED] audio_cb finish skip (tts_playback)")
- return
- except Exception:
- pass
- try:
- if getattr(self, "_ptt_led_lock_listening", 0) == 1:
- return
- except Exception:
- pass
- self._vc_led_sync_from_fsm()
- def set_tts_volume(self,event,msg):
- EventMesh.publish("set_storage_config",["VC_TTS_volume",msg])#音量配置
- self.VC_TTS_volume = msg
- print("set_tts_volume:{} {}".format(self.VC_TTS_volume,msg))
- if int(msg) <= 10: #最小一定要设置1要不然没有声音
- volume = 10 //10
- self.aud.setVolume(volume)
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- pass
- def get_yuyinaudio_flag(self, event=None, msg = None):
- return self.voice_flag
-
- def set_yuyinaudio_flag(self, event=None, msg = None):
- self.voice_flag=0
-
- #留言主动拒听
- def get_pwk_cnt_voice(self, event=None, msg = None):
- return self.pwk_cnt_voice
-
- def set_pwk_cnt_voice(self, event=None, msg = None):
- self.pwk_cnt_voice=msg
- print("设置未读次数为:{}".format(self.pwk_cnt_voice))
-
- #会议
- def get_meetaudio_flag(self, event=None, msg = None):
- return self.meet_flag
- def set_meetaudio_flag(self, event=None, msg = None):
- self.meet_flag = 0
-
- def get_audio_play_state(self, event=None, msg = None):
- return self.audio_play_state
-
-
-
- def come_meet_mode(self, event=None, msg = None):
- if self.meet_flag == 0: #还是为0就提示进入会议纪要模式
- EventMesh.publish("send_meet_open_flag_to_iot")#创建会议
- self.meet_flag=1
- exit_tts ="开启会议纪要,正在录音"
- EventMesh.publish("play_tts",[exit_tts,1])
- utime.sleep_ms(4000)
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press})
- pass
- def exit_meet_mode(self, event=None, msg = None):
- if self.meet_flag == 1: #还是为1就提示已退出会议纪要模式
- EventMesh.publish("send_meet_end_flag_to_iot")#结束会议
- self.meet_flag=0
- EventMesh.publish("stop_voice_record") #关闭语音录音
- exit_tts ="录音结束,已退出会议纪要模式"
- EventMesh.publish("play_tts",[exit_tts,1])
- pass
- def come_trans_mode(self, event=None, msg = None):
- if self.trans_flag == 0: #还是为0就提示进入AI翻译模式
- EventMesh.publish("send_trans_come_flag_to_iot")#进入翻译
- pass
- def exit_trans_mode(self, event=None, msg = None):
- if self.trans_flag == 1: #还是为1就提示已退出AI翻译模式
- EventMesh.publish("stop_voice_record") #关闭语音录音
- exit_tts ="已退出AI翻译模式"
- EventMesh.publish("play_tts",[exit_tts,1])
- self.trans_flag=0
- EventMesh.publish("send_trans_end_flag_to_iot")#退出翻译
- pass
- def set_trans_flag(self,event,msg):
- self.trans_flag = msg
- def exit_voice_message_mode(self, event=None, msg = None):
- if self.voice_flag == 1: #还是为1就提示已退出会议纪要模式
- self.voice_flag=0
- exit_tts ="已退出语音留言模式"
- EventMesh.publish("play_tts",[exit_tts,1])
- pass
- def ai_key_press_audio_cb1(self, event=None, msg = None):
- print("播放按键音!!!!!!!!!!")
- # EventMesh.publish("audio_montage_play",["chenggong.mp3"])
- # self.chenggong_tone()
- self.aud.aud_tone_play(10,400)#播放按键音
- def ai_key_press_audio_cb2(self, event=None, msg = None):
- EventMesh.publish("audio_montage_play",["chenggong.mp3"])
- # self.chenggong_tone() #播放按键音
-
- def AI_key_press(self, event=None, msg = None):#按键按下
- # 按键时打印 LED 聚合状态/最终模式(用于定位“绿灯不灭/闪一下”)
- try:
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
- try:
- led = EventMesh.publish("get_led_debug_state")
- except Exception:
- led = None
- try:
- t = getattr(self.fsm, "turn", None)
- except Exception:
- t = None
- print("[KEY] AI_key_press VC_mode={} {} turn={} led={}".format(
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
- except Exception:
- pass
- # 单对话 PTT / 长对话分支(按键短按提示音已关,避免与开录等提示音叠在一起)
- try:
- if self._ai_key_press_ptt_mode():
- return
- except Exception:
- pass
- if self._ai_key_press_long_dialogue_interrupt():
- return
- try:
- if self._ai_key_press_long_dialogue_when_session_ready():
- return
- except Exception:
- pass
- try:
- if self._ai_key_press_long_dialogue_wake():
- EventMesh.publish(constants.EVENT.SYSTEM_UP)
- return
- except Exception:
- pass
-
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- if EventMesh.publish("get_sos_flag")== 1:
- return
- if self.voice_flag == 1: #语音留言模式
- self.stop_voice_timer.stop() #关闭语音留言模式定时器
- # EventMesh.publish("stop_AI_record")
- self.Close()
- self.revoice_cnt = 0
- self.revoice_timer.stop()#回复定时器关闭,计数器清零
- EventMesh.publish("start_voice_record",2)#开始语音录音
- return
- elif self.meet_flag == 1:#会议纪要模式
- self.Close()
- EventMesh.publish("start_voice_record",3)#开始会议录音
- return
- elif self.trans_flag == 1:#AI翻译模式
- return
-
- EventMesh.publish("stop_voice_record") #关闭语音录音
- print("AI_key_press: unhandled mode branch")
- self.AI_key_long_press_flag = 0
- def BT_AI_key_press(self, event=None, msg = None):#按键按下
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- if EventMesh.publish("get_sos_flag")== 1:
- return
- if self.voice_flag == 1: #语音留言模式
- tts_str = "语音留言模式,长按按钮开始录音,松开按钮结束"
- EventMesh.publish("play_tts",[tts_str,1])
- return
-
- EventMesh.publish("stop_voice_record") #开启静默需要关闭
- print("AI_key_press")
- self.get_config_timer_count = 0
- self.key_state = 1
- # EventMesh.publish("audio_montage_play",["chenggong.mp3"])
- # self.chenggong_tone() #播放按键音
- # 长对话:蓝牙按键同单按
- # - 对话进行中:打断/续聊(复用 dialog_id)
- # - 已停止/未 started:走新会话流程
- try:
- if self.VC_mode == 0:
- 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_client_VC_RequestToSpeak})
- return
- except Exception:
- pass
-
- self.Close()
-
- if self.VC_mode == 1: #长按对话模式 接收到蓝牙耳机按键事件
-
- tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "
- EventMesh.publish("play_tts",[tts_str,1])
- return
- # if dev_type == "7PIN":
- # self.chenggong_tone()
- #self.aud.aud_tone_play(10,200)#播放按键音
- self.AI_TTS_str = ""#"你好,有什么吩咐"
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- # if self.VC_state != VC_State_None:
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- # EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
- self._uplink_request_stop("BT_AI_key_press/compat2")
- self._uplink_request_start("BT_AI_key_press/compat2")
-
- self.AI_VC_start()
-
-
- # self.Close()
- # self.AI_VC_start()
- self.AI_key_long_press_flag = 0
- def stop_AI_record(self, event=None, msg=None):#停止AI对话
- if self.VC_satrt_flag == 1:
- self.get_config_timer_count = 0
- self.AI_TTS_str = ""#"你好,有什么吩咐"
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- self.AI_key_long_press_flag = 0
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- self.request_uplink_stop("stop_AI_record")
- count = 0
- while True:
- if count >= 20 or self.VC_satrt_flag == 0:
- break
- count+=1
- utime.sleep_ms(500)
-
- def exit_voice_message_timer(self,*argc):
- EventMesh.publish("exit_voice_message_mode")
-
- def AI_key_release(self, event=None, msg=None):#按键弹起
- try:
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
- try:
- led = EventMesh.publish("get_led_debug_state")
- except Exception:
- led = None
- try:
- t = getattr(self.fsm, "turn", None)
- except Exception:
- t = None
- print("[KEY] AI_key_release VC_mode={} {} turn={} led={}".format(
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
- except Exception:
- pass
- print("AI_key_release123")
- self.key_state = 0
- if self.trans_flag == 1:#AI翻译模式
- EventMesh.publish("stop_voice_record") #关闭语音录音
- return
- if self.voice_flag == 1:
- self.stop_voice_timer.stop()
- self.voice_flag=0
- if EventMesh.publish("GET_USB_STATE") == 0:
- EventMesh.publish("set_led_state", {"ai_state": "idle"})
- EventMesh.publish("stop_voice_record")#停止静默录音
- tts_play_str = "留言已发送"
- self.voice_flag=0
- EventMesh.publish("play_tts",[tts_play_str,1])
- return
- if self.VC_mode == 1:
- self._ai_key_release_handle_ptt()
- 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})
- except Exception:
- pass
- 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
- 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])
-
- 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):
- try:
- led = EventMesh.publish("get_led_debug_state")
- except Exception:
- led = None
- try:
- t = getattr(self.fsm, "turn", None)
- except Exception:
- t = None
- print("[KEY] AI_key_long_press VC_mode={} {} turn={} led={}".format(
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
- except Exception:
- pass
- print("AI_key_long_press")
- self._ai_key_long_press_route(event, msg)
-
- def pwk_key_release(self, event, msg):#电源键短按,关闭
- self._handle_pwk_short_release(event, msg)
-
-
- def key_prompt(self):
- EventMesh.publish("audio_montage_play",["key_prompt.mp3"])
- #self.aud.play(1,1,'U:/static/key_prompt.mp3')#提示请您按住中间的按钮开始讲话,讲完松开即可
- def VC_error(self, event, msg):#错误播报
- self.websocket_error()
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error})
- def set_VC_record_state_flag(self,data):
-
- self.VC_record_state_flag = data
- print("[VC] record_state_flag={}".format(self.VC_record_state_flag))
- #开始录音,不缓存,直接发
- def VC_Speech_Start_now(self):
- # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)
- self._server_vad_silence = 0
- self.VC_Speech_Start()
- if self.VC_record_state_flag == 1:
- self.VC_record_state_flag =2
- print("[VC] Speech_Start_now record_state={}".format(self.VC_record_state_flag))
- #开始录音,先缓存到buf,等服务器响应再发
- def VC_Speech_Start(self):
- try:
-
- #self.VC_state = VC_State_SpeechStart
- print("[VC] Speech_Start record_state={} AI_ASR_flag={}".format(self.VC_record_state_flag,self.AI_ASR_flag))
- if self.AI_ASR_flag != 0:
- return
-
- if self.VC_record_state_flag == 0:
- # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)
- self._server_vad_silence = 0
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- self._last_asr_result_text = ""
- try:
- self._asr_completed_tone_done = 0
- except Exception:
- pass
-
- self.VC_record_thread_close()#关闭录音线程
- _thread.stack_size(self.Audio_buf_size+8*1024)
- self.VC_record_thread_id = _thread.start_new_thread(self.VC_record_thread,())
- _thread.stack_size(8*1024)
- print("self.VC_record_thread_id:{}".format(self.VC_record_thread_id))
- try:
-
- if self.VC_record_TYPE == VC_record_TYPE_AMR :
- try :
- self.record.stream_start(audio.Record.AMRNB, self.VC_record_samplerate, 0)
- except Exception as e:
- usys.print_exception(e)
- elif self.VC_record_TYPE == VC_record_TYPE_PCM:
- print("record_pcm.stream_read_start()!!!!!!!!!!!!")
- self.record.stream_start(audio.Record.WAV, self.VC_record_samplerate, 0)
- # if self.record_pcm != None:
- # try:
- # self.record_pcm.stream_stop()
- # self.record_pcm.close()
- # self.record_pcm = None
- # except Exception as e:
- # usys.print_exception(e)
- # if self.record_pcm == None:
- # ret = self.record_pcm = audio.Audio.PCM(0,1,16000,0,1,2)
- # print("audio.Audio.PCM:ret:{}".format(ret))
- # ret = self.record_pcm.stream_read_start()
- # print("self.record_pcm.stream_read_start:ret:{}".format(ret))
- except Exception as e:
- usys.print_exception(e)
-
- self.VC_record_state_flag = 1
- # 录音硬件已起:协议侧应对应 audio.create(PTT)或长对话直传 PCM
- try:
- print("[VC][proto] record_start sr={} pcm_type={} VC_mode={} session_started={} "
- "ptt_audio_created={} thread_id={}".format(
- self.VC_record_samplerate, self.VC_record_TYPE, self.VC_mode,
- getattr(self, "_vc_session_started", -1),
- getattr(self, "_ptt_audio_created", -1),
- self.VC_record_thread_id))
- except Exception:
- pass
- # 录音开始:聆听灯(PTT 蓝快闪 / 长对话蓝慢闪)
- try:
- self._vc_publish_led("listening", "VC_Speech_Start")
- except Exception:
- pass
- try:
- if getattr(self, "VC_mode", -1) == 1:
- self._ptt_on_record_hardware_started()
- except Exception:
- pass
-
- except Exception as e:
- self.websocket_error()
- self.VC_record_state_flag = 1
- self.VC_Speech_End()#关闭录音
- usys.print_exception(e)
- #停止录音
- def VC_Speech_End(self):
- try:
- self._uplink_mark_end("VC_Speech_End")
- self.AI_ASR_flag = 0
- self.AI_ASR_str_tmp = ""
- # 勿在此清空 AI_ASR_str:vad.silence 停麦常早于 asr.completed,清空会导致 check_text:len:0 连播 think 音
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr})
-
- if self.VC_record_state_flag == 0:
- return
- #self.AI_client_asr_StopTranscription_requset()#请求停止
- if self.VC_record_TYPE == VC_record_TYPE_AMR:
- self.record.stream_stop()
- self.record.stop()#关闭录音
- Record_read_buf = bytearray(self.Audio_buf_size)
- for i in range(10):
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
- # if VC_AUDIO_FRAME_LOG:
- # print("Record_len:!!!!!!{}".format(Record_len))
- if Record_len <= 0:
- break
- elif self.VC_record_TYPE == VC_record_TYPE_PCM:
- self.record.stream_stop()
- self.record.stop()#关闭录音
- Record_read_buf = bytearray(self.Audio_buf_size)
- for i in range(10):
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
- # if VC_AUDIO_FRAME_LOG:
- # print("Record_len:!!!!!!{}".format(Record_len))
- if Record_len <= 0:
- break
- # if self.record_pcm != None:
- # try:
- # self.record_pcm.stream_stop()
- # self.record_pcm.close()
- # self.record_pcm = None
- # except Exception as e:
- # usys.print_exception(e)
- except Exception as e:
- usys.print_exception(e)
- self.VC_record_thread_close()#关闭录音线程
- if self.AI_record_tmp_queue.empty() != True:
- self.AI_record_tmp_queue.clear()
-
- self.VC_record_state_flag = 0
- # PTT 一轮录音结束:清长按语义,避免后续短按松手误走 end_speech 而不播提示音
- try:
- if getattr(self, "VC_mode", -1) == 1:
- self.AI_key_long_press_flag = 0
- except Exception:
- pass
- # 录音停止:单对话进入思考态(等服务器),勿继续蓝闪聆听
- try:
- if getattr(self, "VC_mode", -1) == 1:
- self._ptt_await_server = 1
- self._vc_publish_led("thinking", "VC_Speech_End/ptt")
- else:
- self._ptt_await_server = 0
- self._vc_publish_led("idle", "VC_Speech_End")
- except Exception:
- pass
- print("[VC] Speech_End record_state={}".format(self.VC_record_state_flag))
- #关闭录音线程
- def VC_record_thread_close(self):
- if self.VC_record_thread_id != 0:
- count =50
- while True:
- count -= 1
- if self.VC_record_thread_lock == 1:
- self.VC_record_thread_lock = 2
- if self.VC_record_thread_lock == 0 or count <=0:
- break
- else:
- print("VC_record_thread_thread going:{}...".format(count))
- utime.sleep_ms(1)
- self.VC_record_thread_lock = 0
- print("VC_record_thread_thread_close")
- if self.VC_record_thread_id != 0:
- try:
- _thread.stop_thread(self.VC_record_thread_id)
- except Exception as e:
- usys.print_exception(e)
- self.VC_record_thread_id = 0
- #获取录音数据
-
- def VC_record_thread(self):
- ##return
- print("[VC] record_thread start")
- count = 0
- RequestToSpeak_flag = 0
- # try:
- # uos.remove('/usr/test.pcm')
- # except Exception as e:
- # usys.print_exception(e)
- #record_data_queue = Queue(100)
- #fd = open('/usr/test.pcm','w')
- # if self.VC_record_TYPE == 1:
- # h_data= [0x23,0x21 ,0x41 ,0x4D ,0x52 ,0x0A ]
- # h_data_b = bytes(h_data)
- # self.AI_client_VC_send(h_data_b)
- AI_record_tmp_queue_len = 0
- vad_buf = bytearray()
- VAD_data_count = 0
- vad_state = 0
- self.VAD_count = 0
- while True:
- try:
- if self.VC_record_thread_lock == 2:
- break
- self.VC_record_thread_lock = 1
- # 死循环接收数据
- #if self.VC_state == VC_State_SpeechStart:
- Record_read_buf = bytearray(self.Audio_buf_size)
-
- Record_len = 0
- if self.VC_record_TYPE == VC_record_TYPE_PCM:#pcm
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
- #utime.sleep_ms(60)
-
- else :
-
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
- utime.sleep_ms(600)
-
- #Record_len = 1024*2
- #print("VC_record_thread:{}".format(Record_len))
- if Record_len > 0:
- read_buf = bytes(Record_read_buf[:Record_len])
- # if VC_AUDIO_FRAME_LOG:
- # print("Record_len:{}".format(Record_len))
- if self.record_data_save_flag == 1:
- if self.record_fd == None:
- try:
- uos.remove('/usr/record_data.pcm')
- except Exception as e:
- pass
- # 二进制写入,避免文本模式导致数据异常
- self.record_fd = open('/usr/record_data.pcm', 'wb')
- self.record_fd_len = 0
- if self.record_fd != None:
- try:
- self.record_fd.write(read_buf)
- print("write record:{}".format(len(read_buf)))
- self.record_fd_len = self.record_fd_len+len(read_buf)
- if self.record_fd_len >=500*1024:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- except Exception as e:
- usys.print_exception(e)
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- # count+=Record_len
- # if count >150*1024:
- # self.record_fd.close()
- #self.record_fd = None
- #VAD检测
- if self.is_VAD == 1:
- vad_state = 0
- tmp_c = 1000
- while True:
- tmp_c -=1
- if tmp_c <=0:
- break
- #vad_buf_len = len(vad_buf)
- if VAD_data_count == 0:#没数据的时候
- vad_buf = Record_read_buf[:self.VAD_data_len]
- VAD_data_count = self.VAD_data_len
- self.vad.custom_vad_reset()#重置vad
- else:#有数据的时候
- if Record_len - VAD_data_count < self.VAD_data_len :#剩余长度不够一次填充,先保留,但不填算法
- vad_buf = Record_read_buf[VAD_data_count:Record_len]
- VAD_data_count = 0
- break
- else:#剩余长度够填一次数据
- vad_buf = Record_read_buf[VAD_data_count:VAD_data_count+self.VAD_data_len]
- VAD_data_count += len(vad_buf)
- #print("vad_buf_len:{},{}".format(len(vad_buf),VAD_data_count))
-
- if len(vad_buf) >= self.VAD_data_len:
- vad_state = self.vad.custom_vad_process(vad_buf,len(vad_buf))#给vad数据
- #print("vad_state:{}".format(vad_state))
-
- if VAD_data_count >= Record_len:
- VAD_data_count = 0
- break
- if vad_state == 1:
- print("检测到VAD=1!!!!!!!!!!!!!")
-
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断
- # RequestToSpeak_flag = 1
-
- self.VAD_count+=1
- if self.VAD_count >= 20:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断
-
- else:
- if self.VAD_count != 0:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Pause,"data":0})#恢复音频播放
- self.VAD_count = 0
- if self._vc_tts_pipeline_busy():
- pass
- if vad_state == 1:
- self.AI_record_tmp_queue.put(read_buf)#入缓存队列
- AI_record_tmp_queue_len += len(read_buf)
-
-
- else:
- VAD_data_count = 0
- if self.VC_record_state_flag <=1 :#没发送前先存起来
- self.AI_record_tmp_queue.put(read_buf)
- AI_record_tmp_queue_len += len(read_buf)
- else:
- AI_record_tmp_queue_len = 0
- while True:
- if self.AI_record_tmp_queue.empty() == True:
- break
- else:
- pass
- # 协议门控:会话未 started 前禁止上行音频(否则 session not initialized)
- if VC_PROTO_GUARDS and self._vc_session_started != 1 and self.record_data_save_flag == 0:
- _ = self.AI_record_tmp_queue.get()
- AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(_))
- continue
- if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):
- self.AI_client_VC_send(self.AI_record_tmp_queue.get()) #发送之前存的数据
- else:
- self._uplink_mark_end("silence_gate(drop_queue)")
- _ = self.AI_record_tmp_queue.get()
- # 当前帧发送前协议门控(可用 VC_PROTO_GUARDS 临时关闭):
- # - session started 前禁止发帧
- # - Push-To-Talk 在 audio.create 前禁止发帧
- if VC_PROTO_GUARDS and (
- (self._vc_session_started != 1 and self.record_data_save_flag == 0) or
- (self.VC_mode == 1 and self._ptt_audio_created != 1 and self.record_data_save_flag == 0)
- ):
- # 未 ready:转入缓存,等待 started 再发(并做容量控制)
- try:
- self.AI_record_tmp_queue.put(read_buf)
- AI_record_tmp_queue_len += len(read_buf)
- while AI_record_tmp_queue_len > 128*1024 and self.AI_record_tmp_queue.empty() != True:
- d = self.AI_record_tmp_queue.get()
- AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(d))
- except Exception:
- pass
- else:
- if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):
- self.AI_client_VC_send(read_buf)
- else:
- self._uplink_mark_end("silence_gate(drop_buf)")
-
-
-
-
-
-
- #print("VC_record_thread:{}".format(len(read_buf)))
- #count+=len(read_buf)
- # if count>=500*1024:#结束
- # self.AI_client_VC_end_speech()
- # break
- # count+=1
- # if count %10 == 0:
- # print("VC_record_thread on line {} asr recv :{} send:{}".format(self.VC_record_state_flag,_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)))
-
- # if self.VC_state >= VC_State_SpeechEnd:
- # break
-
- except Exception as e:
- usys.print_exception(e)
- #break
- self.VC_record_thread_lock = 0
- if self.record_data_save_flag == 1:
- utime.sleep_ms(1)
- else:
- utime.sleep_ms(100)
- #fd.close()
- print("VC_record_thread:exit")
- self.VC_record_thread_id = 0#线程退出
- self.VC_record_thread_lock = 0
-###################################################### VC ###################################################################################
- def websocket_connect_VC(self):
- print("websocket_connect_VC:{} e:{}!!!!!!!!!!!!!!!!!!!!!".format(self.AI_client_VC_isconnect,self.AI_client_VC_debug))
- if self.AI_client_VC_isconnect == 1:
- return
- # 创建VC客户端
- try:
- signal = net.csqQueryPoll()
- if signal == -1 or sim.getStatus() != 1:#无蜂窝/无卡等:弱网提示
- self.websocket_error(play_weak_signal_tone=True)
- return
-
- req_str = ""
- ack = ""
- if self.is_VC_ACK == 1:
- ack = "ack=1"
- lang = "lang=zh-cn"#普通话测试环境
- try:
- if "lang" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:
- lang = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["lang"]
- else:
- print("not lang!!!!!!!!!!!!!!!!!!!!!!")
- except Exception as e:
- usys.print_exception(e)
-
- #女,男,童,粤,川
- self.bot_name = "柒先生"
- try:
- if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:
- self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]
- else:
- print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")
- except Exception as e:
- usys.print_exception(e)
-
- # Headers = {'X-NLS-Token':'0c8b8f05a46f423096be64a9c4c46310'}
- # self.AI_client_VC = uwebsocket.Client.connect('wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1',headers=Headers, debug=False)
- token = EventMesh.publish("get_dev_token")
- print("token:{}".format(token))
-
- req_str = "vc/chat?is_auth={}".format(self.is_auth)
- try:
- if "vc_config" in globals() and vc_config:
- base = vc_config.get_attr("VC_WS_PATH", "vc/chat") or "vc/chat"
- req_str = "{}?is_auth={}".format(base, self.is_auth)
- except Exception:
- pass
- if(token):
- url = 'ws://{}/{}&dt={}'.format(self.server_ip,req_str,token)
- else:
- url = 'ws://{}/{}'.format(self.server_ip,req_str)
- #url = 'ws://115.190.94.208:22080/vc/coze?dt={}'.format(token)
-
- if VC_WS_IO_LOG:
- print("[VC] ws url {}".format(url))
- self.AI_client_VC = uwebsocket.Client.connect(url, debug=False)
- #self.AI_WS_connect_Queue.put(self.AI_client_VC)
-
-
- print("[VC] websocket_connect ok")
- except Exception as e: #网络连接异常(已通过 CSQ/SIM,多为 URL/握手/服务端问题,勿播「信号弱」)
- usys.print_exception(e)
- print("websocket_connect_VC error!!!\r\n")
- try:
- print("[VC] ws connect exc: {}".format(e))
- except Exception:
- pass
- str_error = str(e)
- if str_error.find("[Errno 22] EINVAL")!= -1:
- print("[VC] websocket EINVAL: {}".format(str_error))
- self.websocket_error(play_weak_signal_tone=False)
- EventMesh.publish("set_storage_config",["powerRestart",1])#调试临时关闭
- utime.sleep_ms(1000)
- self.aud.stopPlayStream()
- self.aud.stopAll()
- Power.powerRestart()
-
- self.websocket_error(play_weak_signal_tone=False)
- return
- #print("AI_client_VC:{}".format(self.AI_client_VC))
- if self.AI_client_VC == None:
- EventMesh.publish("update_dev_token")#获取token
- self.websocket_error(play_weak_signal_tone=False)
- return
- self.AI_client_VC_debug+=1
- utime.sleep_ms(10)
- self.AI_client_VC.settimeout(5)
- #self.AI_VC_send_data_queue_thread_close()#关闭发送线程
- if self.AI_VC_send_data_queue_thread_id == 0:
- _thread.stack_size(self.Audio_buf_size+8*1024)
- self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())
- _thread.stack_size(8*1024)
- print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))
- self.AI_client_VC_recv_thread_close()#关闭读线程
- # 线程接收数据
- _thread.stack_size(self.Audio_buf_size+8*1024)
- self.AI_client_VC_recv_id = _thread.start_new_thread(self.AI_client_VC_recv, (self.AI_client_VC,))
- _thread.stack_size(8*1024)
- self.AI_client_VC_isconnect = 1
- print("websocket_connect_VC end")
-
- def websocket_error(self, TTS_str=None, play_weak_signal_tone=False):
- """play_weak_signal_tone=True 时播 xinhaoruo1(弱网/无卡类);连接/协议失败请传 False,避免误报「信号弱」。"""
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] websocket_error enter VC_mode={} TTS_str={} weak_hint={} -> Close then uplink_stop(async)".format(
- getattr(self, "VC_mode", -1), TTS_str, play_weak_signal_tone))
- except Exception:
- pass
- print("websocket_error1!!!!")
- utime.sleep_ms(800)
- self.Close()
- print("websocket_error1.1!!!!")
- print("websocket_error!!!!")
- print("websocket_error!!!!")
- self.VC_Thinking_timerout = 0
- self.VC_satrt_flag = 0
- self.AI_TTS_str = ""
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- self.AI_ASR_flag = 0
- # 可能由收包线程/其它线程触发:统一切回主线程执行 stop,避免“异常→重试→再异常”的循环
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] websocket_error schedule uplink_stop reason=websocket_error + set_led_idle")
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"websocket_error"})
- EventMesh.publish("set_led_state", {"ai_state": "idle"})
- if TTS_str == None:
- self.websocket_error_count+=1
- # if self.websocket_error_count <=3:
- # EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])
- if play_weak_signal_tone and self.websocket_error_count < 10:
- EventMesh.publish("audio_montage_play", ["xinhaoruo1.mp3"])
- elif self.websocket_error_count >=10:
-
- EventMesh.publish("paly_iccid_imei")
- # else:
- # EventMesh.publish("audio_montage_play",["R_SIM_FAILE.mp3"])
- #self.aud.play(1,1,'U:/static/R_SIM_FAILE.mp3')
- if self.websocket_error_count <3 :
- utime.sleep_ms(3000)
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- else:
- print("websocket_error:tts:{}".format(TTS_str))
- self.AI_TTS_str = TTS_str
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- self.Close_flag = 1
- print("websocket_error2{}".format(self.websocket_error_count))
-
-
- print("websocket_error4")
-
-
- def websocket_deconnect_VC(self):
- #print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))
- print("websocket_deconnect_VC!!!!!!!")
- self._vc_session_started = 0
- # 断链/模式切换时必须重置“上行录音状态”,否则会被 _uplink_request_start 的幂等门控挡住,
- # 表现为:切回长对话后无法自动开麦,按键 cancel/canceled 也无法恢复。
- try:
- self.VC_record_state_flag = 0
- except Exception:
- pass
- try:
- self._server_vad_silence = 0
- except Exception:
- pass
- try:
- self._ptt_audio_created = 0
- except Exception:
- pass
- try:
- # 若录音线程/录音设备仍在占用,先停掉
- self.VC_Speech_End()
- except Exception:
- try:
- self.VC_record_thread_close()
- except Exception:
- pass
- # 断链必须同步重置 FSM,否则 await_played/turn 残留会导致动态切换后“无法自动开麦/按键无效”
- try:
- self.fsm.session = VCSessionState.Disconnected
- self.fsm.turn = VCTurnState.Idle
- self.fsm.await_played = False
- self.fsm.last_tts_completed_ms = 0
- self.fsm.last_llm_ms = 0
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- self.AI_client_VC_recv_thread_close()#关闭读线程
- self.AI_client_VC_recv_id = 0
- if self.AI_client_VC_isconnect == 1 or self.AI_client_VC!=None:
- try:
- self.AI_client_VC_isconnect = 0
- count = 500
- while True:#等待发完数据
- count-=1
- if count <=0 or self.AI_VC_send_data_queue_thread_lock == 0:
- break
- utime.sleep_ms(1)
-
- try:
- self.AI_client_VC.close()
- self.AI_client_VC_debug -=1
- #self.AI_client_VC=None
- except Exception as e:
- usys.print_exception(e)
- try :
- self.AI_client_VC.close()
- self.AI_client_VC_debug -=1
- #self.AI_client_VC=None
- except Exception as e:
- usys.print_exception(e)
- try :
- self.AI_client_VC._close()
- except Exception as e:
- usys.print_exception(e)
- self.AI_client_VC = None
- except Exception as e:
- usys.print_exception(e)
- print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))
- #websocket发送ping
- def AI_client_VC_ping(self):
- pass
- if self.AI_client_VC_isconnect == 1:
- #self.AI_client_VC.write_frame(0x09,b'ping ping')
- self.AI_client_VC_send("ping")
- if VC_HEARTBEAT_LOG:
- print("VC: ping ping")
-
- #websocket发送数据
- def AI_client_VC_send(self,data_str):
- # if isinstance(data_str, str):#是字符串
- # print("AI_client_VC_send:{}".format(data_str))
- # pass
- # else:
- # print("AI_client_VC_send:len{}".format(len(data_str)))
- # 统计“语音上行”真实开始/结束:这里只统计二进制音频(非 ping/字符串)
- try:
- if not isinstance(data_str, str):
- l = len(data_str)
- if l > 0:
- self._uplink_mark_start(l)
- self._uplink_bytes += l
- except Exception:
- pass
- self.AI_VC_send_data_queue.put([AI_client_VC_num,data_str])
- #关闭数据发送线程
- def AI_VC_send_data_queue_thread_close(self):
- if self.AI_VC_send_data_queue_thread_id != 0:
- # 先清空待发队列,避免关闭过程持续堆积/刷屏
- if self.AI_VC_send_data_queue.empty() != True:
- self.AI_VC_send_data_queue.clear()
- # 通知发送线程退出;若发送线程正阻塞在底层 send,则只能等超时或强制 stop_thread
- self.AI_VC_send_data_queue_thread_lock = 2
- count = 500
- while True:
- count -= 1
- if self.AI_VC_send_data_queue_thread_lock == 0 or count <=0:
- break
- else:
- # 该打印只是“等待线程停下来”的调试心跳,默认关闭以免影响系统实时性
- if VC_HEARTBEAT_LOG and (count % 50 == 0 or count > 490):
- print("AI_VC_send_data_queue_thread going:{}...".format(count))
- utime.sleep_ms(5)
- self.AI_VC_send_data_queue_thread_lock = 0
- if VC_HEARTBEAT_LOG:
- print("AI_VC_send_data_queue_thread_close")
- if self.AI_VC_send_data_queue_thread_id != 0:
- try:
- # 线程可能已自然退出/被复用,stop_thread 会抛 ValueError
- try:
- running = _thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)
- except Exception:
- running = True
- if running:
- _thread.stop_thread(self.AI_VC_send_data_queue_thread_id)
- except Exception as e:
- usys.print_exception(e)
- self.AI_VC_send_data_queue_thread_id = 0
- #发送数据队列
- def AI_VC_send_data_queue_thread(self):
- print("AI_VC_send_data_queue_thread:start")
- count_t = 0
- while True:
- if self.AI_VC_send_data_queue_thread_lock == 2:
- print("AI_VC_send_data_queue_thread,该退出了")
- break
- if self.AI_VC_send_data_queue.empty() :#不能去掉,去掉会卡死
- self.AI_VC_send_data_queue_thread_lock = 0
- count_t+=1
- # if count_t%200 == 0:
- # print("self.AI_VC_send_data_queue.empty")
- utime.sleep_ms(1)
- else:
- #data[0]是websocket连接对象编号,data[1]是数据
- #print("AI_VC_send_data_queue_thread:get")
-
- self.AI_VC_send_data_queue_thread_lock = 1
- data = self.AI_VC_send_data_queue.get()
-
-
- #print("AI_VC_send_data_queue_thread:num{} len{}".format(data[0],len(data[1])))
- if data != None:
- try:
- #data["func"](data["data"])
- # 发送数据
- #bytes_data = bytes(data[1])
- #data[0].send(data[1])
- #print("AI_VC_send_data_queue_thread:len:{}".format(len(data[1])))
- #print("AI_VC_send_data_queue_thread on line asr recv :{} vc recv:{}".format(_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_client_VC_recv_id)))
- if data[0] == AI_client_VC_num:
-
- if isinstance(data[1], str):#是字符串
- self.VC_pingping_timerout = 0
- s = data[1]
- if s is None:
- s = ""
- # 保活 ping 不落 WS IO 日志(避免周期性刷屏)
- if VC_WS_IO_LOG and s != "ping":
- try:
- if len(s) > 240:
- preview = s[:160] + " ... " + s[-60:]
- else:
- preview = s
- print("[VC] WS send_txt len={} txt={}".format(len(s), preview))
- except Exception:
- try:
- print("[VC] WS send_txt {}".format(data[1]))
- except Exception:
- pass
- elif not VC_WS_IO_LOG:
- if s == "ping":
- pass
- else:
- try:
- ev, tid = _vc_out_json_event_meta(s)
- if ev:
- print("[VC][proto] tx event={} trace_id={} len={} ws_conn={}".format(
- ev, tid or "-", len(s), self.AI_client_VC_isconnect))
- else:
- print("[VC][proto] tx unknown_json len={} ws_conn={}".format(
- len(s), self.AI_client_VC_isconnect))
- except Exception:
- print("[VC][proto] tx parse_fail len={}".format(len(s)))
- else:
- pass
- # if VC_AUDIO_FRAME_LOG:
- # print("VC: send_data len:{}".format(len(data[1])))
- # print("self.AI_client_VC = {} , self.AI_client_VC_isconnect = {}".format(self.AI_client_VC,self.AI_client_VC_isconnect))
- if self.AI_client_VC != None and self.AI_client_VC_isconnect == 1:
- self.AI_client_VC.send(data[1])
- else:
- try:
- if isinstance(data[1], str) and data[1] != "ping":
- _ev, _tid = _vc_out_json_event_meta(data[1])
- print("VC VC: send_data:ERROR len={} event={} trace_id={}".format(
- len(data[1]), _ev or "?", _tid or "-"))
- else:
- print("VC VC: send_data:ERROR:{}".format(len(data[1])))
- except Exception:
- print("VC VC: send_data:ERROR:{}".format(len(data[1])))
- #print("VC: send_data end")
-
- #print("VC: send_data end")
- except Exception as e:
- print("DeviceInfo_queue_thread:ERROR:{}".format(e))
- usys.print_exception(e)
- if data[0] == AI_client_VC_num:
- #self.websocket_error()
- pass
-
-
- try:
- if self.AI_client_VC_isconnect != 0:
- print("VC发送数据错误,退出")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC发送出错了,请重试"})
- self.ud_error(["VC",self.VC_taskid])
- except Exception as e: #网络连接异常
- usys.print_exception(e)
-
- utime.sleep_ms(1)
- self.AI_VC_send_data_queue_thread_lock = 0
- #VC关闭读线程
- def AI_client_VC_recv_thread_close(self):
- if self.AI_client_VC_recv_id != 0:
- count =500
- while True:
- count -= 1
- if self.AI_client_VC_recv_lock == 1:
- self.AI_client_VC_recv_lock = 2
- if self.AI_client_VC_recv_lock == 0 or count <=0:
- break
- else:
- print("AI_client_VC_recv_thread going:{}...".format(count))
- utime.sleep_ms(1)
- self.AI_client_VC_recv_lock = 0
- print("AI_client_VC_recv_thread_close")
- if self.AI_client_VC_recv_id!= 0:
- try:
- _thread.stop_thread(self.AI_client_VC_recv_id)
- except Exception as e:
- usys.print_exception(e)
- self.AI_client_VC_recv_id = 0
- #Ai VC websocket返回数据
- def AI_client_VC_recv(self,cli):
- count = 0
- asr_led_flag = 0
- tts_led_flag = 0
- while True:
- try:
- # 死循环接收数据
- self.AI_client_VC_recv_lock = 0
-
- count+=1
- if self._vc_tts_pipeline_busy() and (self.tts_in_data_len - self.tts_out_data_len > 64*1024 or count>5):
- count = 0
- utime.sleep_ms(1)
- if not self._vc_tts_pipeline_busy():
- utime.sleep_ms(1)
- recv_data = cli.recv()
- if self.AI_client_VC_recv_lock == 2:
- print("AI_client_VC_recv 该退出了")
- break
- self.AI_client_VC_recv_lock = 1
- if self._vc_rx_resets_long_idle_timer(recv_data):
- self.close_timerout = 0
- if recv_data == "websocket close":
- # 服务器关闭连接
- print("VC 服务器关闭连接!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- #self.AI_client_VC_isconnect = 0
- #self.websocket_deconnect_VC()
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr})
- # #EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(1500)})
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_connect_asr})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC连接断开了,请重试"})
- self.ud_error(["VC",self.VC_taskid])
- break
- elif recv_data == "":#客户端关闭连接
- print("VC VC :客户端关闭连接")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC连接断开了,请重试"})
- self.ud_error(["VC",self.VC_taskid])
- break
- try:
- self.websocket_error_count = 0
- if isinstance(recv_data, str):#返回的是字符串
- jsload_data = ujson.loads(recv_data)
- if VC_WS_IO_LOG:
- print("[VC] recv raw:{}".format(recv_data))
- else:
- try:
- _rev = jsload_data.get("event", "")
- if _rev != "llm.delta":
- print("[VC] rx event={} sid={}".format(
- _rev, jsload_data.get("sid", "") or "-"))
- except Exception:
- pass
- #print("AI_client_VC_recv jsload_data = {}".format(jsload_data))
- try:
- self._cur_sid = jsload_data.get("sid", "") or ""
- except Exception:
- self._cur_sid = ""
- try:
- ev = jsload_data.get("event", "")
- if ev:
- self._vc_dispatch_server_event(ev, jsload_data)
- except Exception:
- pass
-
- if jsload_data["event"] == "started" :#会话开始
- #payload = jsload_data["payload"]
- # self.VC_dialog_id = payload["dialog_id"]
- # self.VC_task_id = jsload_data["task_id"]
- # started 仅表示链路就绪:不在这里提前切灯态;
- # 聆听灯只在“录音真正开始/停止”处更新(VC_Speech_Start/End)。
- # 会话开始:允许上行;连续对话模式下直接进入“可发送音频”状态
- self._vc_session_started = 1
- # started 时不要强制清 Close_flag:
- # - Close_flag==1:提示音/模式切换播完退出
- # - Close_flag==0:正常对话流程
- self._ptt_audio_created = 0
- self._server_vad_silence = 0
- self._turn_has_asr_completed = 0
- 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
- try:
- if getattr(self, "_skip_started_uplink_once", 0) == 1:
- self._skip_started_uplink_once = 0
- _skip_uplink = 1
- print("[VC][uplink_ctrl] Deny START by server.started cause=boot_prompt")
- except Exception:
- pass
- if not _skip_uplink:
- # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后
- # 出现首轮无音频上行导致“第一次对话没有”的情况。
- if self.record_data_save_flag == 0:
- if self.VC_record_state_flag == 0:
- self.request_uplink_start("server.started")
- else:
- self._vc_clear_stale_record_state("server.started")
- self.request_uplink_start("server.started/retry")
-
- print("VC:Started!!!!!!!!\r\n")
- print("TTS音频为:{},语音下发地址为:{}".format(self.AI_TTS_str,self.AI_new_voice_url))
- 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:必须先同步发出 audio.create,再开麦。
- # request_send_audio_create() 走 EventMesh 异步队列,会导致录音线程先于
- # audio.create 上行,深度睡眠唤醒后首轮 ASR 空结果、第二轮才正常。
- try:
- self.AI_client_VC_send_speech()
- except Exception:
- self.request_send_audio_create()
- # 若长按发生在“未 started”窗口,收到 started 后立即开麦进入新一轮 PTT
- try:
- if getattr(self, "_ptt_force_record_on_started", 0) == 1:
- self._ptt_force_record_on_started = 0
- 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})
-
- elif jsload_data["event"] == "config.update" :#配置更新中,需要停止录音
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- self.request_uplink_stop("config.update")
-
- elif jsload_data["event"] == "asr.started" :#asr开始
- try:
- self.fsm.on_server_asr_started()
- except Exception:
- pass
- self._server_vad_silence = 0
- self._turn_has_asr_completed = 0
- self._turn_has_vad_silence = 0
- self._turn_has_tts_started = 0
- self._llm_completed_ms = 0
- try:
- self._asr_completed_tone_done = 0
- except Exception:
- pass
- # 单对话:_drop_tts_audio 在 VC_Speech_Start 真开录后再清 0,避免 canceled 后仍播旧 TTS
- try:
- if getattr(self, "VC_mode", 0) != 1:
- self._drop_tts_audio = 0
- except Exception:
- pass
- # 单对话:勿在 asr.started 置 flag=2(未真开麦会僵死);由 VC_Speech_Start 维护
- self.LocalRespondingEnded_timerout = 0
- elif jsload_data["event"] == "asr.result" :#asr结果
- try:
- self.AI_ASR_str = jsload_data["payload"]["content"]
- except Exception:
- try:
- self.AI_ASR_str = jsload_data.get("payload", {}).get("text", "") or ""
- except Exception:
- self.AI_ASR_str = ""
- print("[VC][ASR] {}".format(self.AI_ASR_str))
- try:
- if (self.AI_ASR_str or "").strip():
- self._last_asr_result_text = self.AI_ASR_str
- except Exception:
- pass
- if self.AI_ASR_str == "":#没识别到
- self.AI_TTS_str = "我没听清,请再说一遍"
- self._vc_led_alert_rb_flash()
- elif jsload_data["event"] == "asr.completed" :#asr 终止的标识
- self._turn_has_asr_completed = 1
- try:
- if not (self.AI_ASR_str or "").strip():
- self.AI_ASR_str = getattr(self, "_last_asr_result_text", "") or ""
- except Exception:
- pass
- # 本地 think 提示:仅在有有效 ASR 文本时播,避免空轮次连播「滴」
- try:
- if getattr(self, "_asr_completed_tone_done", 0) == 1:
- pass
- elif not self._vc_effective_asr_text():
- try:
- print("[VC][ASR] completed empty, skip think tone")
- except Exception:
- pass
- else:
- stream_on = False
- try:
- stream_on = EventMesh.publish("get_vc_tts_playback_active") == 1
- except Exception:
- stream_on = False
- pcm_busy = getattr(self, "play_pcm", None) is not None
- playing = getattr(self, "VC_Play_flag", 0) == 1
- try:
- 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):
- self._vc_local_play('U:/static/think.mp3', 1, 1, 650)
- self._asr_completed_tone_done = 1
- except Exception:
- pass
- print("self.commands_mode:{}".format(self.commands_mode))
- if self.commands_mode ==2:
- ret= self.check_text(self._vc_effective_asr_text())
- if ret == 0:#配置指令
-
- 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()
- self.AI_ASR_send_buf_len = 0
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- try:
-
- self.request_uplink_stop("asr.completed/config_cmd")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- continue
- elif ret == 1:#不处理
- pass
- #self.VC_Speech_End()
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
-
- # if self.record_data_save_flag == 1:
- # if self.record_fd :
- # self.record_fd.close()
- # self.record_fd = None
-
- # if dev_type != "7PIN" :
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.send_tone})
- elif jsload_data["event"] == "vad.active" :#VAD 检测到说话→聆听
- 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
- 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")
- elif jsload_data["event"] == "llm.created" :#思考(LLM)
- try:
- self._ptt_await_server = 0
- except Exception:
- pass
- try:
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- except Exception:
- pass
- self._llm_completed_ms = 0
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
- pass
- elif jsload_data["event"] == "llm.completed" :#llm完成(可能无 tts)
- try:
- self._llm_completed_ms = utime.ticks_ms()
- except Exception:
- # 勿用 1:ticks_diff(now,1)≈开机至今,会瞬间满足 thinking 超时误以为该重启
- self._llm_completed_ms = 0
- try:
- self._cur_dialog_id = jsload_data.get("payload", {}).get("dialog_id", "") or ""
- except Exception:
- self._cur_dialog_id = ""
- try:
- self.fsm.log("server.llm.completed")
- except Exception:
- pass
- pass
- elif jsload_data["event"] == "llm.delta" :#llm返回数据
- if self.is_VAD == 0 and self.record_data_save_flag == 0:
- if self.VC_record_state_flag == 1:#llm返回数据还没停止,就先停止
- self.request_uplink_stop("llm.delta")
- elif jsload_data["event"] == "tts.started" :#tts开始
- try:
- self._ptt_await_server = 0
- except Exception:
- pass
- self._server_vad_silence = 1
- self._turn_has_tts_started = 1
- self._tts_pending = 0
- self._tts_pending_ms = 0
- self._uplink_mark_end("tts.started")
- # stop uplink + play_start 由 FSM action list 统一触发,避免多处重复调用
- # 标记本轮 TTS 来源:
- # - 有 payload.dialog_id:认为是“对话回复 TTS”
- # - 无 dialog_id:认为是“提示/提醒 TTS”(模式切换可通过 _pending_tts_kind 标记)
- try:
- did = jsload_data.get("payload", {}).get("dialog_id", "") or ""
- except Exception:
- did = ""
- # 注意:无 dialog_id 的 tts.started 很可能是提示音/提醒音;
- # 但“模式切换提示音”的 kind 可能已在 synthesis 时写入 _last_tts_kind,
- # 这里不应无条件覆盖为 prompt,否则播完无法触发下一轮开麦。
- try:
- if did:
- self._last_tts_kind = "dialog"
- else:
- k = getattr(self, "_pending_tts_kind", "") or ""
- if k:
- self._last_tts_kind = k
- else:
- cur = getattr(self, "_last_tts_kind", "") or ""
- if cur in (
- "mode_switch_long",
- "mode_switch_ptt",
- "boot_prompt",
- ):
- pass
- elif not cur:
- self._last_tts_kind = "prompt"
- except Exception:
- pass
- try:
- self._pending_tts_kind = ""
- except Exception:
- pass
- # 播放灯由阶段事件控制
- elif jsload_data["event"] == "tts.completed" :#tts结束
- # tts.completed 仅表示下发结束,本地可能仍在播放;绿灯关闭在本地播放结束时处理
- try:
- # server event 已在统一入口 on_server_event 触发 FSM,这里避免重复驱动
- pass
- except Exception:
- pass
- try:
- try:
- sid = getattr(self, "_cur_sid", "")
- did = getattr(self, "_cur_dialog_id", "")
- except Exception:
- sid, did = "", ""
- print("[TTS] completed in_len={} out_len={} q_empty={} play_pcm={} sid={} did={}".format(
- getattr(self, "tts_in_data_len", 0),
- getattr(self, "tts_out_data_len", 0),
- 1 if self.AI_audio_queue.empty() else 0,
- 1 if self.play_pcm != None else 0,
- sid,
- did,
- ))
- except Exception:
- pass
- # 播完上报完全由 FSM await_played + play_end 驱动,这里不再使用 LocalRespondingEnded_timerout/_need_audio_played_ack
- pass
-
- elif jsload_data["event"] == "canceled" :#打断
- # 停播/清队列已在 _vc_dispatch_server_event(canceled) 完成;此处仅收尾标志
- self.is_ruppt_tts = 0#设备上发中断tts,收到服务器返回的cancel后置为0
- self._need_audio_played_ack = 0
- self._tts_pending = 0
- self._tts_pending_ms = 0
- # 开麦由 FSM action list 统一触发(uplink_start:canceled),避免重复调用
- if self.AI_audio_queue_thread_id ==0:
- _thread.stack_size(24*1024)
- self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())
- _thread.stack_size(8*1024)
- print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))
- else:
- print("AI_VC_start :AI_audio_queue_thread_id != 0 ")
- elif jsload_data["event"] == "completed" :#会话结束(可能无 TTS,如 ASR 空)
- # 重要:服务端常在 tts.completed 后紧跟 completed。
- # 若本轮存在 TTS/待发 audio.played,则不能在此清零 LocalRespondingEnded_timerout/_need_audio_played_ack,
- # 否则会掐断「播完→audio.played→下一轮录音」链路,表现为最多 1~2 轮就停。
- if self._need_audio_played_ack == 1 or self._vc_tts_pipeline_busy() or self.VC_Play_flag == 1:
- pass
- else:
- # 无 TTS 的 completed:清状态并按需进入下一轮(见下方条件)
- self.LocalRespondingEnded_timerout = 0
- self._need_audio_played_ack = 0
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- # 连续对话:若本轮没有 TTS(或服务端直接 completed),需要立刻回到聆听状态
- # 否则会出现“有时 1 轮/2 轮后就停住”的现象(服务端 completed 但端侧未重新开录音上行)。
- if self.VC_mode == 0 and self.Close_flag == 0:
- # 仅当“本轮已结束一次语句(vad.silence 或 asr.completed)且从未进入 tts.started”
- # 才自动进入下一轮;避免把真正的“会话结束/异常结束”误当成下一轮入口。
- if (self._turn_has_tts_started == 0) and (self._turn_has_vad_silence == 1 or self._turn_has_asr_completed == 1):
- self._server_vad_silence = 0
- # 开麦由 FSM action list 统一触发(uplink_start:completed),这里不再重复调用
- pass
-
- elif jsload_data["event"] == "command" :#命令匹配
- ret = -1
- try:
- if self.commands_mode ==1:
- if len(jsload_data["payload"]["functions"])>0:
-
- cmd = jsload_data["payload"]["functions"][0]["function"]["arguments"]
- print("llm:cmd:{}".format(cmd))
- cmd = ujson.loads(cmd)
- ret = self.check_text_llm(cmd)
- elif self.commands_mode ==0:
-
- cmd = jsload_data["payload"]
- print("server:cmd:{}".format(cmd))
- ret = self.check_text_server(cmd)
- else:
- cmd = jsload_data["payload"]["functions"][0]["function"]["arguments"]
- print("llm:cmd:{}".format(cmd))
- cmd = ujson.loads(cmd)
- ret = self.check_text_llm(cmd)
- except Exception as e:
- usys.print_exception(e)
- if ret == -1:
- if self.AI_TTS_str =="":
- self.AI_TTS_str = "抱歉,没找到对应操作,可以换个说法试试"
- #EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
- self.request_uplink_stop("command")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- elif jsload_data["event"] == "failed":#接口返回错误
- if jsload_data["payload"]["error"] == "concurrency_limit_exceeded":
- EventMesh.publish("publish_AI_btnInfo",{"func":self.concurrency_limit_exceeded_tone})
-
- #utime.sleep_ms(4000)
-
- else:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":jsload_data["payload"]["message"]})
- pass
-
- elif jsload_data["event"] == "audio.convert.completed":#音频转成完成没有出错 接口
- # self.VC_state = VC_State_End
- print("未读留言初始数值:{}".format(self.pwk_cnt_voice))
- if self.pwk_notice_flag == 1:#只有在播放的时候才会去计数
- if self.pwk_cnt_voice > 0:
- self.pwk_cnt_voice -= 1#减1
- print("未读留言又-1,当前留言剩余数量为:{}".format(self.pwk_cnt_voice))
- else:
- print("未读列表为空")
- if self.pwk_cnt_voice == 0:#积累的留言已经全部播完
- self.pwk_notice_flag = 0
- self.wait_sort = 0
- self.wait_first_flag = 0#让下次未读播放提示恢复
- self.voicerupt_flag = 0 #标志位置0
- self.wait_voicerupt_flag = 0
- self.voice_notice = 1#开启回复
- if self.history_voice_flag == 1 and self.wait_voice_flag == 0:
- self.voice_notice = 0
- EventMesh.publish("set_storage_config",["is_noread_voice",0])
-
- if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭
- self.voice_choose_flag = 0
- self.Close_flag = 1
- self.LocalRespondingEnded_timerout = 8
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- if self.voice_notice == 1:#已收到留言,进行该留言回复逻辑处理
- tts_play_str = "如需回复,长按确认键开始录制,松开发送。十秒内无操作自动退出"
- EventMesh.publish("play_tts",[tts_play_str,1])
- self.voice_notice = 0#标志位清空
- self.voice_timer.stop()
- self.revoice_cnt = 0
- self.voice_flag = 1
- print("语音留言回复停止")
- self.revoice_timer.start(1000,1,self.revoice_judge)
- print("是否未读:{},是否播完:{}".format(EventMesh.publish("get_wait_voice_flag"),len(EventMesh.publish("get_play_new_voice_list"))))
- # if EventMesh.publish("get_wait_voice_flag") == 1 and len(EventMesh.publish("get_play_new_voice_list")) ==0:#还在未读并且列表为空
- # self.wait_first_flag = 0
- # print("未读留言播完,清空标志")
- # EventMesh.publish("set_wait_voice_flag",0)
- # EventMesh.publish("set_history_voice_flag",0)
- EventMesh.publish("update_play_new_voice_info")#更新已读状态
- self.recv_voice = 0
- print("不开启定时器标志位置0")
- self.LocalRespondingEnded_timerout = 8
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
-
- pass
- elif jsload_data["event"] == "audio.convert.failed": #音频转换失败了
- pass
-
-
- else:#下发音频数据
- #self.AI_ASR_str = ""#下发音频后清掉asr结果
-
-
- # PTT 打断后丢弃残留 TTS 音频帧,避免“断断续续继续播”
- try:
- if self._ptt_should_drop_inbound_tts():
- continue
- except Exception:
- pass
- if self.VC_stop_play_audio_flag == 0:
- print("新的AI对话不允许以前的播放数据:")
- break
-
- # if VC_AUDIO_FRAME_LOG:
- # print("下发音频数据1:{}".format(len(recv_data)))
-
- if self.play_data_save_flag == 1:
- if self.play_fd ==None:
- try:
- uos.remove('/usr/play_data.pcm')
- except Exception as e:
- usys.print_exception(e)
- self.play_fd = open('/usr/play_data.pcm','w')
- if self.play_fd !=None:
- self.play_fd.write(recv_data)
- # 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才修改标志位
- if self.VC_play_TYPE == VC_play_TYPE_PCM:
- if self.play_pcm != None:
- try:
- #print("play_pcm.stream_write !!!")
- buf_len = self.play_pcm.stream_get_buf_len()
- count_tmp = 1000
- while buf_len < len(recv_data):
- utime.sleep_ms(1)
- count_tmp-=1
- if count_tmp<=0 or self.play_pcm == None:
- break
- buf_len = self.play_pcm.stream_get_buf_len()
- #print("buf已满,等待释放:buf_len:{},count_tmp:{}".format(buf_len,count_tmp))
- except Exception as e:
- usys.print_exception(e)
- EventMesh.publish("publish_AI_btnInfo",{"func":self.put_record_data,"data":[recv_data,""]})
- else:
- print("被打断不播放")
- utime.sleep_ms(1)
- except Exception as e:
- usys.print_exception(e)
-
- except Exception as e:
- error_str = str(e)
- #print("VC VC:e:{}".format(error_str))
- if error_str.find("[Errno 110] ETIMEDOUT")!= -1 or error_str.find("[Errno 115] ")!= -1:
- if VC_HEARTBEAT_LOG:
- print("VC VC:recv 正常报错:{}".format(error_str))
- # recv 超时仅表示本周期无帧,链路仍可能在等 LLM/TTS;清零避免误触发「接口无响应」
- self.close_timerout = 0
- pass
- else:
- usys.print_exception(e)
- #self.websocket_error()
- print("VC VC :读数据报错,退出")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC出错了,请重试"})
- self.ud_error(["VC",self.VC_taskid])
- break
- self.AI_client_VC_isconnect = 0
- self.AI_client_VC_recv_id = 0
- self.AI_client_VC_recv_lock = 0
- print("[VC] recv_thread exit")
-
- def voice_play_10S(self,event,msg):#按键开启留言播放等待10S定时器判断
- self.voice_timer.start(1000,1,self.voice_judge)
- #语音留言定时器
- def voice_judge(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.voice_judger_cb})
- def voice_judger_cb(self):
- self.voice_cnt += 1
- # print("语音留言定时器cnt = {}".format(self.voice_cnt))
- if self.voicerupt_flag == 1:#按键被按下
- self.pwk_notice_flag = 1#表示进入收听未听留言
- self.AI_VC_start()
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
- self.voice_cnt = 0
- # self.recv_voice = 0#进入留言定时器,留言清空
- print("进入留言定时器1,留言清空!!!!!")
- EventMesh.publish("update_play_new_voice_info")#更新已读状态
- self.voice_timer.stop()
- if self.pwk_cnt_voice > 0:
- EventMesh.publish("get_new_voice_msg_S")
- if self.voice_cnt > 15:#10S后退出播放,5S是为了给语音提示留时间
- self.voice_cnt = 0
- self.recv_voice = 0#进入留言定时器,留言清空
- if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭
- self.voice_choose_flag = 0
- print("进入留言定时器2,留言清空!!!!!")
- self.voice_timer.stop()
- #语音留言回复定时器
- def revoice_judge(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.revoice_judger_cb})
- def revoice_judger_cb(self):
- self.revoice_cnt += 1
- # print("语音留言回复定时器cnt = {}".format(self.revoice_cnt))
- if self.revoice_cnt > 20:#10S后退出播放,10S是为了给语音提示留时间
- self.revoice_cnt = 0
- self.voice_flag=0
- EventMesh.publish("set_1M_nowplay_flag",0)
- print("语音留言回复停止")
- self.revoice_timer.stop()
- #音乐播放定时器
- def music_play(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.music_play_cb})
- def music_play_cb(self):
- self.music_cnt += 1
- if self.music_cnt > 4:#4S后播放音乐,4S是为了给语音提示留时间
- self.AI_VC_start()
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
- self.music_cnt = 0
- self.music_timer.stop()
- #历史留言播放定时器
- def history_play(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.history_play_cb})
- def history_play_cb(self):
- self.his_cnt += 1
- if self.his_cnt > 4:#4S后播放,4S是为了给语音提示留时间
- self.AI_VC_start()
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
- self.his_cnt = 0
- self.his_timer.stop()
- #未读留言是否播放定时器
- def wait_play(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_play_cb})
- def wait_play_cb(self):
- self.wait_cnt += 1
- if self.wait_voicerupt_flag == 1:#按键被按下
- EventMesh.publish("set_is_listen_wait",1)#已听
- self.pwk_notice_flag = 1#表示进入收听未听留言
- self.AI_VC_start()
- self.wait_sort = 1
- self.AI_TTS_str = "第"+str(self.wait_sort)+"条"
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
- # self.wait_cnt = 0
- # print("进入留言定时器1,留言清空!!!!!")
- # EventMesh.publish("update_play_new_voice_info")#更新已读状态
- self.wait_timer.stop()
- if self.wait_cnt > 10:#10S后退出播放,5S是为了给语音提示留时间
- self.wait_cnt = 0
- self.recv_voice = 0#进入留言定时器,留言清空
- EventMesh.publish("set_history_voice_flag",0)
- EventMesh.publish("set_wait_voice_flag",0)
- EventMesh.publish("set_open_device",0)#开机不听
- self.wait_first_flag = 0
- if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭
- self.voice_choose_flag = 0
- print("进入留言定时器2,留言清空!!!!!")
- self.wait_timer.stop()
- #未读留言一条条播放定时器
- def wait_single_play(self, *args, **kwargs):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_single_play_cb})
- def wait_single_play_cb(self):
- self.wait_cnt += 1
- if self.wait_cnt > 2:#2S后播放,2S是为了给语音提示留时间
- self.AI_VC_start()
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
- self.wait_cnt = 0
- self.wait_timer.stop()
- def set_voicerupt_flag(self,event,msg):#按键控制当前留言播放
- self.voicerupt_flag = msg
- def set_wait_voicerupt_flag(self,event,msg):#按键控制未读留言播放
- self.wait_voicerupt_flag = msg
- def get_voicerupt_flag(self,event,msg):#获取按键控制留言播放状态
- return self.voicerupt_flag
- def get_recv_voice(self,event,msg):
- return self.recv_voice
- def set_recv_voice(self,event,msg):
- return self.recv_voice
- def put_record_data(self,data):
- try:
- if self._ptt_should_drop_inbound_tts():
- return
- except Exception:
- pass
- #print("下发音频数据1:{}".format(len(data[0])))
- ret=self.AI_audio_queue.put(data)
- try:
- if self.tts_in_data_len == 0 and data and len(data[0]) > 0:
- try:
- sid = getattr(self, "_cur_sid", "")
- did = getattr(self, "_cur_dialog_id", "")
- except Exception:
- sid, did = "", ""
- print("[TTS] first_chunk len={} play_pcm={} VC_Play_flag={} sid={} did={}".format(
- len(data[0]),
- 1 if self.play_pcm != None else 0,
- getattr(self, "VC_Play_flag", 0),
- sid,
- did,
- ))
- except Exception:
- pass
- self.tts_in_data_len += len(data[0])
- #print("下发音频数据2:{},{}".format(len(data[0]),self.tts_in_data_len))
- #msg[0]是tts播放字符串,msg[1]是是否关闭当前tts
- 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:
- tts_str = data[0]
- is_close_VC = data[1]
- if tts_str != "":
- self.AI_TTS_str = tts_str
- if is_close_VC != None:
- self.Close_flag = is_close_VC
- 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"})
- else:
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- except Exception:
- pass
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_tts_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
-
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- pass
- def check_text_server(self,cmd):
- return -1
-
- def check_text_llm(self,cmd):
- try:
- text = cmd["command"]["chinese_description"]
- if text.find("切换音色") != -1:
- text = cmd["command"]["params_json"]
- try:
- for key in self.VC_TTS_voice_type:
- if text.find(key) != -1 :
- self.VC_TTS_voice_key = key
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- except Exception as e:
- usys.print_exception(e)
- if text.find("男") != -1 :
- if "男" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "男"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- if text.find("女") != -1 :
- if "女" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "女"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- if text.find("同声") != -1 or text.find("童") != -1 :
- if "童" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "童"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- if text.find("粤") != -1 or text.find("越野") != -1 or text.find("广东话") != -1:
- if "粤" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "粤"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- if text.find("四川") != -1 :
- if "四川" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "四川"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- if text.find("个性") != -1 :
- if "个性" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "个性"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- if text.find("英语") != -1 :
- if "英语" in self.VC_TTS_voice_type:
- self.VC_TTS_voice_key = "英语"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
-
- self.AI_TTS_str = "了解,声音已切换"
- return 0
- self.AI_TTS_str = "很抱歉,没找到对应声音"
- return 0
- if text.find("切换对话模式") != -1:
- if self.VC_mode == 0:
- self.VC_mode = 1
- self.AI_TTS_str = "了解,已切换到单对话模式"
- else:
- self.VC_mode = 0
- self.AI_TTS_str = "了解,已切换到长对话模式"
- EventMesh.publish("set_storage_config",["VC_mode",self.VC_mode])
-
-
- return 0
-
- if text.find("音量调整") != -1:
- text = cmd["command"]["params_json"]
- text = ujson.loads(text)
- volume = text["n"]
- if volume >100 or volume<0:
- self.AI_TTS_str = "音量调整超出范围了,只能0-100哦"
- else:
- self.VC_TTS_volume = volume
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- self.AI_TTS_str = "了解,音量已经调整,当前音量是{}".format(self.VC_TTS_volume)
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- return 0
- if text.find("音量增大") != -1:
- print("self.VC_TTS_volume add:{}".format(self.VC_TTS_volume))
- self.VC_TTS_volume += 10
- if self.VC_TTS_volume > 100:
-
- self.AI_TTS_str = "音量已经是最大啦"
- self.VC_TTS_volume = 100
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- self.AI_TTS_str = "了解,音量已经调大了,当前音量是{}".format(self.VC_TTS_volume)
-
- return 0
- if text.find("音量减小") != -1:
- print("self.VC_TTS_volume end:{}".format(self.VC_TTS_volume))
- self.VC_TTS_volume -= 10
- if self.VC_TTS_volume < 0:
- self.VC_TTS_volume = 0
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
- self.AI_TTS_str = "音量已经是最小啦"
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- self.AI_TTS_str = "好嘞,音量已经调小了,当前音量是{}".format(self.VC_TTS_volume)
- return 0
-
- if text.find("语速增大") != -1:
- self.speech_rate += 10
- if self.speech_rate > 50:
-
- self.AI_TTS_str = "语速已经是最快了"
- self.speech_rate = 50
- else:
-
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
-
- self.AI_TTS_str = "明白,语速已经调快了,当前语速是{}".format(self.speech_rate)
-
- return 0
- if text.find("语速减小") != -1:
- self.speech_rate -= 10
- if self.speech_rate < -50:
- self.speech_rate = -50
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
- self.AI_TTS_str = "语速已经是最慢了"
- else:
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
-
- self.AI_TTS_str = "明白,语速已经调慢了,当前语速是{}".format(self.speech_rate)
- return 0
-
- if text.find("进入工厂模式") != -1 :
- self.factory_mode()
- return 1
- if text.find("查询") != -1:
- if text.find("信号") != -1 :
- signal = net.csqQueryPoll()
-
- self.AI_TTS_str = "当前信号{}".format(signal)
- return 0
- if text.find("电量") != -1 :
- battery = EventMesh.publish("get_battery")
- print("电量查询:{}".format(battery))
- #EventMesh.publish("audio_montage_play",["dianliangshengyu{}.mp3".format(battery)])
- #self.aud.play(1,0,'U:/static/dianliangshengyu{}.mp3'.format(battery))#电量剩余
- self.AI_TTS_str = "电量剩余百分之{}".format(battery)
- return 0
- if text.find("电压") != -1 :
- Vbatt = Power.getVbatt()
- #EventMesh.publish("audio_montage_play",["dianliangshengyu{}.mp3".format(battery)])
- #self.aud.play(1,0,'U:/static/dianliangshengyu{}.mp3'.format(battery))#电量剩余
- self.AI_TTS_str = "当前电池电压{}".format(Vbatt)
- return 0
-
- if text.find("卡号") != -1 :
- iccid = sim.getIccid()
- if iccid != -1 and len(iccid) > 5:
- print("iccid{}".format(iccid))
- self.AI_TTS_str = "当前卡号为:{}".format(iccid)
- else:
- self.AI_TTS_str = "卡号查询失败"
- return 0
- if text.find("设备号") != -1 :
- imei = modem.getDevImei()
- if imei != -1 and len(imei) > 5:
- print("IMEI:{}".format(imei))
- self.AI_TTS_str = "当前设备号为:{}".format(imei)
- else:
- self.AI_TTS_str = "设备号查询失败"
- return 0
- if text.find("版本") != -1 :
- bt_versions = EventMesh.publish("get_bt_versions")
- if bt_versions =="" :
- bt_versions = "BT_NONE"
- self.AI_TTS_str ="当前版本号:"+version_util.get_tts_version(bt_versions)
- return 0
- if text.find("服务器地址") != -1 :
- self.AI_TTS_str ="服务器地址"+self.server_ip
- return 0
- if text.find("升级") != -1 and (text.find("OTA") != -1 or text.find("ota") != -1):
- if self.ai_OTA.min_sys_ota()==1:
- self.AI_TTS_str = "有新版本,准备开始升级,升级过程会多次重启设备,请保证流量充足及电量充足"
- else:
- self.AI_TTS_str = "当前已是最新版本"
- return 0
-
- if len(text)<20 and (text.find("再见") != -1 or text.find("拜拜") != -1 or text.find("关闭对话") != -1):
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- self.AI_TTS_str = "好嘞,再见,我们下次再聊"
- self.Close_flag = 1
- return 0
- if len(text)<12 and (text.find("关机") != -1 ):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- EventMesh.publish("Power_off")
- return 1
- if text.find("闹钟设定") != -1:
-
- # {
- # "event":"command",
- # "payload":{
- # "dialog_id":"d5ef6fc424834bc4915107c76b2c6d7e_1750671489126798340",
- # "functions":[
- # {
- # "id":"56867702528670853",
- # "type":"function",
- # "function":{
- # "name":"device_action",
- # "arguments":"{\"command\":{\"chinese_description\":\"闹钟设定\",\"english_description\":\"alarm\",\"fail_prompt\":\"设定闹钟失败\",\"params_json\":\"{\\\"alarm\\\": [{\\\"alarm_content\\\": \\\"无\\\", \\\"alarm_timer\\\": \\\"7:00\\\", \\\"alarm_type\\\": \\\"单次\\\", \\\"repeat_day\\\": [\\\"2025-06-24\\\"], \\\"repeat_min\\\": 0, \\\"valid_time\\\": \\\"\\\"}]}\",\"succeed_prompt\":\"设定闹钟成功\"}}"
- # }
- # }
- # ]
- # },
- # "sid":"d5ef6fc424834bc4915107c76b2c6d7e"
- # }
-
- text = cmd["command"]["params_json"]
- print("params_json:{}".format(text))
-
- try:
- alarm = ujson.loads(text)
- add_alarm_tmp = EventMesh.publish("AI_add_alarm",alarm["alarm"])
- if add_alarm_tmp[0]> 0:
- self.AI_TTS_str = add_alarm_tmp[1]
- return 1
- except Exception as e:
- usys.print_exception(e)
- if text.find("取消闹钟") != -1: #清空所有闹钟
- try:
- self.AI_TTS_str = EventMesh.publish("AI_delete_alarm")
- except Exception as e:
- usys.print_exception(e)
- return 0
- if text.find("闹钟列表") != -1 :
- EventMesh.publish("read_alarm_list")
- return 0
-
- except Exception as e:
- usys.print_exception(e)
- self.AI_TTS_str = cmd["command"]["fail_prompt"]
- return -1
- def check_text(self,text):
- print("check_text:len:{}".format(len(text)))
- if len(text)>60:
- return -1
- try:
- if isinstance(text, str):
- # if dev_type != 'jswm' and dev_type != 'jswm_BOX':#金三完美,不使用设备切换角色
- # if len(text)<25 and (text.find("你好") != -1 or text.find("您好") != -1) :
- # try:
- # for key in self.VC_TTS_voice_type:
- # if text.find(key) != -1 :
- # self.VC_TTS_voice_key = key
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # self.AI_TTS_str = "嗨,{}来了,有什么吩咐".format(self.VC_TTS_voice_key)
- # return 0
- # except Exception as e:
- # usys.print_exception(e)
- # return -1
- # if text.find("声音") != -1 and text.find("改") != -1:
- # try:
- # for key in self.VC_TTS_voice_type:
- # if text.find(key) != -1 :
- # self.VC_TTS_voice_key = key
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # self.AI_TTS_str = "更改成功"
- # return 0
- # except Exception as e:
- # usys.print_exception(e)
- # if text.find("男") != -1 :
- # if "男" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "男"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # if text.find("女") != -1 :
- # if "女" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "女"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # if text.find("同声") != -1 or text.find("童") != -1 :
- # if "童" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "童"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # if text.find("粤") != -1 or text.find("越野") != -1 or text.find("广东话") != -1:
- # if "粤" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "粤"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # if text.find("四川") != -1 :
- # if "四川" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "四川"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # if text.find("个性") != -1 :
- # if "个性" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "个性"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # if text.find("英语") != -1 :
- # if "英语" in self.VC_TTS_voice_type:
- # self.VC_TTS_voice_key = "英语"
- # EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- # # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- # #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- # self.AI_TTS_str = "更改成功"
- # return 0
- # self.AI_TTS_str = "更改失败,没找到对应声音"
- # return 0
- if text.find("切换对话模式") != -1:
- if self.VC_mode == 0:
- self.VC_mode = 1
- try:
- self.AI_TTS_str = vc_config.get_attr("CMD_TOGGLE_MODE_TO_PTT_TTS", "已切换到单对话模式")
- except Exception:
- self.AI_TTS_str = "已切换到单对话模式"
- else:
- self.VC_mode = 0
- try:
- self.AI_TTS_str = vc_config.get_attr("CMD_TOGGLE_MODE_TO_LONG_TTS", "已切换到长对话模式")
- except Exception:
- self.AI_TTS_str = "已切换到长对话模式"
- EventMesh.publish("set_storage_config",["VC_mode",self.VC_mode])
- # EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
-
- return 0
- if text.find("音量") != -1:
- if text.find("加") != -1 :
- self.VC_TTS_volume += 10
- if self.VC_TTS_volume > 100:
- #EventMesh.publish("audio_montage_play",["yinliangyizuida.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuida.mp3')#音量已最大
- self.AI_TTS_str = "音量已最大"
- self.VC_TTS_volume = 100
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- #EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#配置成功
- self.AI_TTS_str = "配置成功,当前音量 {}".format(self.VC_TTS_volume)
-
- return 0
- if text.find("减") != -1:
- self.VC_TTS_volume -= 10
- if self.VC_TTS_volume < 0:
- self.VC_TTS_volume = 0
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
- self.AI_TTS_str = "音量已最小"
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- #EventMesh.publish("audio_montage_play",["peizhichenggong.mp3"])
- #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#配置成功
- self.AI_TTS_str = "配置成功,当前音量 {}".format(self.VC_TTS_volume)
- return 0
-
- if text.find("进入工厂模式") != -1 :
- self.factory_mode()
- return 1
- if text.find("查询") != -1:
-
- if text.find("信号") != -1 :
- signal = net.csqQueryPoll()
-
- self.AI_TTS_str = "当前信号{}".format(signal)
- return 0
- if text.find("电量") != -1 :
- battery = EventMesh.publish("get_battery")
- print("电量查询:{}".format(battery))
- #EventMesh.publish("audio_montage_play",["dianliangshengyu{}.mp3".format(battery)])
- #self.aud.play(1,0,'U:/static/dianliangshengyu{}.mp3'.format(battery))#电量剩余
- self.AI_TTS_str = "电量剩余百分之{}".format(battery)
- return 0
- if text.find("电压") != -1 :
- Vbatt = Power.getVbatt()
- #EventMesh.publish("audio_montage_play",["dianliangshengyu{}.mp3".format(battery)])
- #self.aud.play(1,0,'U:/static/dianliangshengyu{}.mp3'.format(battery))#电量剩余
- self.AI_TTS_str = "当前电池电压{}".format(Vbatt)
- return 0
-
- # if text.find("卡号") != -1 :
- # iccid = sim.getIccid()
- # if iccid != -1 and len(iccid) > 5:
- # print("iccid{}".format(iccid))
- # self.AI_TTS_str = "当前卡号为:{}".format(iccid)
- # else:
- # self.AI_TTS_str = "卡号查询失败"
- # return 0
- # if text.find("设备号") != -1 :
- # imei = modem.getDevImei()
- # imei = num_str_to_chinese(imei)#转为中文
- # if imei != -1 and len(imei) > 5:
- # print("IMEI:{}".format(imei))
- # self.AI_TTS_str = "当前设备号为:{}".format(imei)
- # else:
- # self.AI_TTS_str = "设备号查询失败"
- # return 0
- if text.find("版本") != -1 :
- bt_versions = EventMesh.publish("get_bt_versions")
- if bt_versions ==None:
- bt_versions = "BT_NONE"
- self.AI_TTS_str ="当前版本号:"+version_util.get_tts_version(bt_versions)
-
- return 0
- if text.find("服务器地址") != -1 :
- self.AI_TTS_str ="服务器地址"+self.server_ip
- return 0
-
- if text.find("讲话") != -1 or text.find("说话") != -1:
- if text.find("快") != -1 and text.find("太") == -1 :
- self.speech_rate += 10
- if self.speech_rate > 50:
-
- self.AI_TTS_str = "语速已经是最快了"
- self.speech_rate = 50
- else:
-
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
-
- self.AI_TTS_str = "明白,语速已经调快了,当前语速是{}".format(self.speech_rate)
-
- return 0
- if text.find("慢") != -1 and text.find("太") == -1 :
- self.speech_rate -= 10
- if self.speech_rate < -50:
- self.speech_rate = -50
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
- self.AI_TTS_str = "语速已经是最慢了"
- else:
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
-
- self.AI_TTS_str = "明白,语速已经调慢了,当前语速是{}".format(self.speech_rate)
- return 0
- if len(text)<22 and(text.find("聆听") != -1 or text.find("明天")!= -1) :
- #print("text len :{}".format(len(text)))
- if text.find("快") != -1 and text.find("太") == -1 :
- self.vad_silence -= 200
- if self.vad_silence < 100:
-
- self.AI_TTS_str = "已经是最快了"
- self.vad_silence = 100
- else:
- self.AI_TTS_str = "配置成功,当前语音断句速度是{}毫秒".format(self.vad_silence)
- EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])
- return 0
- if text.find("慢") != -1 and text.find("太") == -1 :
- self.vad_silence += 200
- if self.vad_silence > 2000:
- self.vad_silence = 2000
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
- self.AI_TTS_str = "已经是最慢了"
- else:
- self.AI_TTS_str = "配置成功,当前语音断句速度是{}毫秒".format(self.vad_silence)
- EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])
- return 0
- if text.find("升级") != -1 and (text.find("OTA") != -1 or text.find("ota") != -1):
- if self.ai_OTA.min_sys_ota()==1:
- self.AI_TTS_str = "有新版本,准备开始升级,升级过程会多次重启设备,请保证流量充足及电量充足"
- else:
- self.AI_TTS_str = "当前已是最新版本"
- return 0
- try:
- bt_voice_en = ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_VOICE_FEATURES_ENABLED", False))
- except Exception:
- bt_voice_en = False
- if bt_voice_en:
- if text.find("语音留言") != -1:
- self.voice_flag = 1
- self.AI_TTS_str = "已进入语音留言模式,长按按钮开始录音,松开按钮结束"
- self.Close_flag = 1
- self.stop_voice_timer.stop()
- self.stop_voice_timer.start(30*1000,0,self.exit_voice_message_timer)
- print("语音留言!!!!!!!!!!")
- return 0
- if text.find("开广播") != -1 or text.find("开蓝牙广播") != -1:
- self.AI_TTS_str = "正在开蓝牙广播"
- EventMesh.publish("AI_send_bt_ble_switch_toble",1)
- print("正在开蓝牙广播")
- return 0
- if text.find("关广播") != -1 or text.find("关蓝牙广播") != -1:
- self.AI_TTS_str = "正在关蓝牙广播"
- EventMesh.publish("AI_send_bt_ble_switch_toble",2)
- print("正在关蓝牙广播")
- return 0
- if text.find("开经典蓝牙") != -1:
- self.AI_TTS_str = "正在开经典蓝牙"
- EventMesh.publish("AI_send_bt_ble_switch_toble",3)
- print("正在开经典蓝牙")
- return 0
- if text.find("关闭经典蓝牙") != -1 or text.find("关经典蓝牙") != -1:
- self.AI_TTS_str = "正在关经典蓝牙"
- EventMesh.publish("AI_send_bt_ble_switch_toble",4)
- print("正在关经典蓝牙")
- return 0
- if text.find("确认解绑设备") != -1 or text.find("确认设备解绑") != -1:
- self.AI_TTS_str = "设备解绑中...."
- self.Close_flag = 1
- EventMesh.publish("send_unbind_dev_to_iot")
- print("解绑设备!!!!!!!!!!!!!!")
- return 0
- if text.find("删除耳机记录") != -1 or text.find("清楚耳机记录") != -1:
- self.AI_TTS_str = "正在删除"
- EventMesh.publish("clear_earphone_record")
- print("正在删除")
- return 0
- # if len(text)<16 and (text.find("再见") != -1 or text.find("拜拜") != -1 or text.find("关闭对话") != -1):
- # #EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- # if dev_type != "7PIN" and dev_type != "7PIN_coze" and dev_type != "7PIN_1_5":#柒牌
- # self.AI_TTS_str = "好嘞,再见,我们下次再聊"
- # self.Close_flag = 1
- # return 0
- # else:
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- # #EventMesh.publish("sleep_BT")#休眠蓝牙
- # return 1
- if len(text)<12 and (text.find("关机") != -1 ):
- try:
- close_only = ("vc_config" in globals() and vc_config and vc_config.get_bool("POWEROFF_CMD_CLOSE_ONLY", True))
- except Exception:
- close_only = True
- if close_only:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- return 1
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- EventMesh.publish("Power_off")
- return 1
- except Exception as e:
- usys.print_exception(e)
- return -1
- #获取trace_id
- def get_trace_id(self):
- t = utime.mktime(utime.localtime())
- t = t*1000+urandom.randint(0, 1000)
- md5_data = "{}".format(t)#md5签名
- sign_data = ""
- hash_obj = uhashlib.md5(md5_data)
- res = hash_obj.digest()
-
- # 转成十六进制表示
- sign_data = ubinascii.hexlify(res)
- sign_data = sign_data.decode('ascii')
- return sign_data
-
- def get_session_id(self,data = "session_id"):
- t = utime.mktime(utime.localtime())
- t = t*1000+urandom.randint(0, 1000)
- md5_data = "{}{}".format(data,t)#md5签名
- sign_data = ""
- hash_obj = uhashlib.md5(md5_data)
- res = hash_obj.digest()
-
- # 转成十六进制表示
- sign_data = ubinascii.hexlify(res)
- sign_data = sign_data.decode('ascii')
- return sign_data
-
- def get_new_message_id(self):
- message_id = self.get_trace_id()+"aaa"+self.get_trace_id()
- self.VC_message_id=message_id[:32]
- return self.VC_message_id
- def get_new_task_id(self):
- task_id = self.get_trace_id()+"aaa"+self.get_trace_id()
- self.VC_task_id=task_id[:32]
- return self.VC_task_id
- #开始播放声音
- def VC_Play_Start(self):
- try:
- if self._ptt_should_skip_remote_playback():
- if self._vc_led_state_debug_enabled():
- print("[VC] VC_Play_Start skip (ptt_interrupt)")
- return
- except Exception:
- pass
- self.tts_in_data_len = 0
- self.tts_out_data_len = 0
- if self.VC_Play_flag == 0:
- try:
- _ptt_fast_open = False
- try:
- if getattr(self, "VC_mode", -1) == 1:
- _ptt_fast_open = True
- except Exception:
- pass
- # 本地提示音/文件播放可能占用同一音频通道,导致 TTS 流播放“写入成功但无声”;
- # 在开启 TTS PCM 前先停掉本地播放,避免资源冲突。
- try:
- self.aud.stopPlayStream()
- self.aud.stopAll()
- except Exception:
- pass
- # 资源互斥:播放前确保录音上行已停止(无 AEC 场景)
- try:
- if self.record_data_save_flag == 0 and self.VC_record_state_flag != 0:
- self._uplink_request_stop("VC_Play_Start")
- utime.sleep_ms(30)
- except Exception:
- pass
- if self.VC_play_TYPE == VC_play_TYPE_PCM:
- if self.play_pcm == None:
- # 刚停采集时延后打开播音 PCM,且失败时重试,降低「pcm open write only fail」
- last_err = None
- _open_retries = 1 if _ptt_fast_open else 3
- for i in range(_open_retries):
- try:
- if not _ptt_fast_open:
- utime.sleep_ms(80 + i * 60)
- self.play_pcm = audio.Audio.PCM(0, 1, self.VC_play_samplerate, 1, 1, 2)
- ret = self.play_pcm.stream_write_start()
- print("self.record_pcm.stream_write_start:ret:{}".format(ret))
- last_err = None
- break
- except Exception as e:
- last_err = e
- try:
- if self.play_pcm != None:
- self.play_pcm.close()
- except Exception:
- pass
- self.play_pcm = None
- if last_err:
- raise last_err
- elif self.VC_play_TYPE == VC_play_TYPE_MP3:
- pass
- self.VC_Play_flag = 1
- try:
- if not self._ptt_should_skip_remote_playback():
- self._vc_publish_led("playing", "VC_Play_Start/tts")
- except Exception:
- pass
- except Exception as e:
- self.VC_Play_End()
- usys.print_exception(e)
- #暂停/继续声音播放 Pause_flag = 1暂停
- def VC_Play_Pause(self,Pause_flag):
- try:
- if self.VC_play_TYPE == VC_play_TYPE_PCM:
- if self.play_pcm != None:
- self.play_pcm.stream_pause(Pause_flag)
- except Exception as e:
- self.VC_Play_End()
- usys.print_exception(e)
- #停止播放声音
- def VC_Play_End(self):
- self.VC_Play_flag = 0
- if self.VC_play_TYPE == VC_play_TYPE_PCM:
- if self.VC_play_samplerate == 8000:
- self.aud.stopPlayStream()
- self.aud.stopAll()
- elif self.VC_play_samplerate == 16000:
- if self.play_pcm != None:
- print("Close:play_pcm.close()!!!!!!!!!!!!!!!!!!")
- try:
- self.play_pcm.stream_stop()
- self.play_pcm.close()
- except Exception as e:
- usys.print_exception(e)
- self.play_pcm=None
-
- self.aud.stopPlayStream()
- self.aud.stopAll()
- elif self.VC_play_TYPE == VC_play_TYPE_MP3:
- self.aud.stopPlayStream()
- self.aud.stopAll()
- if self.AI_audio_queue.empty()!=True:
- self.AI_audio_queue.clear()
-
- # TTS 播完:按真实 FSM/录音态恢复灯;本地短音不走此路径
- try:
- if getattr(self, "_vc_play_end_suppress_idle_led", 0):
- if self._vc_led_state_debug_enabled():
- print("[LED] VC_Play_End skip led (suppress)")
- elif getattr(self, "VC_record_state_flag", 0) != 0:
- self._vc_publish_led("listening", "VC_Play_End/recording")
- else:
- self._vc_led_sync_from_fsm()
- except Exception:
- pass
- # 关键日志:定位“tts 结束绿灯仍不灭”究竟是 base 态(如 charging/full)还是 playing 未退出
- try:
- if "vc_config" in globals() and vc_config and (
- vc_config.get_bool("LED_DEBUG", False) or vc_config.get_bool("KEY_LED_DEBUG", False) or vc_config.get_bool("HW_STATUS_DEBUG", False)
- ):
- try:
- led = EventMesh.publish("get_led_debug_state")
- except Exception:
- led = None
- print("[VC] VC_Play_End led={}".format(led))
- except Exception:
- pass
-
-
- def _1S_timer_cb(self,*argc):
- EventMesh.publish("publish_AI_btnInfo",{"func":self._1S_timer_cb_})
-
- def _fsm_timeout_table_tick(self, now_ms):
- # 兜底统一入口:按“状态机状态 + 超时阈值 + 节流”驱动,避免兜底抢跑/多入口打架。
- # 只在对应 turn/session 状态触发动作。
- try:
- turn = getattr(self.fsm, "turn", VCTurnState.Idle)
- except Exception:
- turn = VCTurnState.Idle
-
- # 1) synthesis→tts.started 超时:只在 Thinking 期允许清 pending 并重启会话
- # 根因:服务端未返回 tts.started 时,继续“回到聆听/开麦”会把本轮状态机拉扯得更乱;
- # 这里直接重启会话,让协议重新进入 started→下一轮流程。
- try:
- if turn == VCTurnState.Thinking and self._tts_pending == 1 and self._tts_pending_ms:
- try:
- if self._fb_last_tts_pending_timeout_ms:
- tts_gap_ok = utime.ticks_diff(now_ms, self._fb_last_tts_pending_timeout_ms) > 15000
- else:
- tts_gap_ok = True
- except Exception:
- tts_gap_ok = True
- if utime.ticks_diff(now_ms, self._tts_pending_ms) > 8000 and tts_gap_ok:
- self._fb_last_tts_pending_timeout_ms = now_ms
- print("[VC][tts_pending] timeout, clear pending and restart session")
- self._tts_pending = 0
- self._tts_pending_ms = 0
- if self.VC_mode == 0 and self.Close_flag == 0:
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
- except Exception:
- pass
-
- # 2) thinking 超时:只在「llm.completed 已收到但 tts.started 长时间未到」时才重启会话
- # 注意:不能以 llm.created 的时间戳作为超时基准,否则会在 LLM 正常推理期间误判超时(notts 场景)。
- try:
- if self.VC_mode == 0 and self.Close_flag == 0:
- llm_completed_ms = getattr(self, "_llm_completed_ms", 0)
- # 只有在 llm.completed 之后才允许超时重启
- if turn == VCTurnState.Thinking and llm_completed_ms and self._turn_has_tts_started == 0:
- age_ms = utime.ticks_diff(now_ms, llm_completed_ms)
- restart_threshold_ms = 20000
- # _fb_last_thinking_restart_ms==0 时 ticks_diff(now,0) 非预期(间隔被误判极大),
- # 会在 llm.completed 后首批 tick 就触发 AI_VC_start → notts(无 tts.started);首次仅信任 age 门限
- try:
- if self._fb_last_thinking_restart_ms:
- gap_ok = utime.ticks_diff(now_ms, self._fb_last_thinking_restart_ms) > 15000
- else:
- gap_ok = True
- except Exception:
- gap_ok = True
- if (age_ms > restart_threshold_ms and gap_ok and
- self._tts_pending == 0 and self._need_audio_played_ack == 0 and self.VC_Play_flag == 0):
- self._fb_last_thinking_restart_ms = now_ms
- self._llm_completed_ms = 0
- self._server_vad_silence = 0
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- try:
- print("[VC][thinking_timeout] AI_VC_start age_ms={} thr={}".format(
- age_ms, restart_threshold_ms))
- except Exception:
- pass
- self._vc_led_alert_rb_flash()
- # thinking_timeout 策略:重启会话(而不是抢开麦),避免干扰服务端 LLM/TTS 流程
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
- except Exception:
- pass
-
- def _1S_timer_cb_(self):
- try:
- self._vc_tick_serial_cancel_timeout()
- except Exception:
- pass
-
- EventMesh.publish("system_sleep_rest")#防止黑屏
- # --- Level 1:仅长对话(VC_mode==0),无业务下行 idle_sec 后结束会话(需再次按键进入)
- # close_timerout:保活清除策略见 _vc_rx_resets_long_idle_timer(排除文本 ping/pong、heartbeat 等)
- # 与浅休眠/灭屏解耦;单对话不走本计时
- idle_sec = 20
- try:
- if "vc_config" in globals() and vc_config:
- idle_sec = int(vc_config.get_int("VC_IDLE_AUTO_CLOSE_SEC", 20))
- except Exception:
- idle_sec = 20
- if idle_sec < 5:
- idle_sec = 5
- if self.VC_mode == 0 and self.VC_satrt_flag == 1 and getattr(self, "AI_client_VC_isconnect", 0) == 1:
- self.close_timerout += 1
- else:
- self.close_timerout = 0
- if self.VC_mode == 0 and idle_sec > 0 and self.close_timerout > idle_sec:
- try:
- print("[VC][idle_L1] 长对话无业务下行超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={} {}".format(
- self.close_timerout, idle_sec, self.VC_mode,
- getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1),
- self._vc_fsm_log_snip()))
- except Exception:
- pass
- if self._vc_long_idle_active_speech_or_later():
- try:
- print("[VC][idle_L1] 处理=websocket_error(对话中无服务端响应)")
- except Exception:
- pass
- print("[VC][long_idle] branch=speech_no_rsp -> websocket_error + ud_error")
- print("接口无响应,超时退出")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"接口超时了。请重试"})
- self.ud_error(["VC",self.VC_taskid])
- else:
- try:
- print("[VC][idle_L1] 处理=Close(结束对话,需再次按键进入)")
- except Exception:
- pass
- print("[VC][long_idle] branch=idle_timeout -> Close")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- #EventMesh.publish("sleep_BT")#休眠蓝牙
-
- # --- Level 2:长对话+单对话,业务 idle 累计 VC_IDLE_DEEP_SLEEP_SEC → 断开(若仍连)+ 深睡事件
- deep_sec = 60
- deep_en = 1
- try:
- if "vc_config" in globals() and vc_config:
- deep_sec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))
- deep_en = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))
- except Exception:
- deep_sec, deep_en = 60, 1
- active_deep = bool(deep_en) and deep_sec > 0
- if active_deep and deep_sec < 10:
- deep_sec = 10
- if not active_deep:
- self._idle_deep_sec = 0
- elif not self._vc_is_business_idle():
- self._idle_deep_sec = 0
- try:
- self._vc_idle_deep_armed_suppress = 0
- except Exception:
- pass
- elif getattr(self, "_vc_idle_deep_armed_suppress", 0):
- pass
- else:
- self._idle_deep_sec += 1
- if active_deep and self._idle_deep_sec > deep_sec:
- try:
- print("[VC][idle_L2] 业务idle累计超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={}".format(
- self._idle_deep_sec, deep_sec, self.VC_mode,
- getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1)))
- except Exception:
- pass
- try:
- self._vc_idle_deep_armed_suppress = 1
- except Exception:
- pass
- self._idle_deep_sec = 0
- EventMesh.publish("publish_AI_btnInfo", {"func": self._vc_idle_deep_sleep_impl})
-
- # 统一兜底表(按状态机状态触发)
- try:
- self._fsm_timeout_table_tick(utime.ticks_ms())
- except Exception:
- pass
-
- # (不做额外兜底)动态切换依赖 FSM 状态正确清理后,由 server.tts.completed→local.play_end 驱动自动开麦
-
- # print("get_audio_state !!!!!{} {} {}\r\n".format(self.play_end,EventMesh.publish("get_audio_state"),self.play_flag))
- # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====
- try:
- if getattr(self.fsm, "await_played", False):
- # PTT(VC_mode==1) 也必须走本段:旧逻辑整段包在 VC_mode!=1 内会导致 tts.completed 后仍 await_played,
- # 无人触发 VC_Play_End,绿灯/playing 卡死至按键。
- # 不能只用 stream_is_empty:队列仍可能有待播数据时提前关播会“没声”;in/out 字节在分帧下可能未完全对齐,
- # 在已 tts.completed 且队列已空时,以服务端结束下发 + PCM 空为准。
- is_pcm_empty = (self.play_pcm == None) or (self.play_pcm != None and self.play_pcm.stream_is_empty() == 1)
- try:
- q_empty = self.AI_audio_queue.empty()
- except Exception:
- q_empty = True
- try:
- in_len = getattr(self, "tts_in_data_len", 0)
- out_len = getattr(self, "tts_out_data_len", 0)
- except Exception:
- in_len, out_len = 0, 0
- try:
- tts_done_ms = getattr(self.fsm, "last_tts_completed_ms", 0)
- except Exception:
- tts_done_ms = 0
- drained_strict = (in_len > 0 and out_len >= in_len and q_empty)
- drained_after_server = (bool(tts_done_ms) and q_empty)
- drained = drained_strict or drained_after_server
- # 8s 超时:兜底认为播放结束(避免播放状态卡死)
- too_long = False
- try:
- t0 = getattr(self.fsm, "last_tts_completed_ms", 0)
- if t0:
- too_long = utime.ticks_diff(utime.ticks_ms(), t0) > 8000
- except Exception:
- pass
- if (is_pcm_empty and drained) or too_long:
- EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_End})
- self._fsm_apply_actions(self.fsm.on_local_play_end())
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- except Exception:
- pass
- if self.online_kws == 1:
- if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:#要打开录音上传数据实现kws
- pass
-
- # thinking/llm→tts 等兜底已在 _fsm_timeout_table_tick 内统一处理
- # VC WebSocket 文本 ping:间隔由 default_config 的 VC_WS_PING_INTERVAL_SEC 控制(秒),<=0 表示关闭 ping
- try:
- _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 4))
- except Exception:
- _ping_iv = 4
- if _ping_iv > 0:
- self.VC_pingping_timerout += 1
- if self.VC_pingping_timerout >= _ping_iv:
- self.VC_pingping_timerout = 0
- self.AI_client_VC_ping()
- # self.asr_pingping_timerout += 1
- # if self.asr_pingping_timerout>3:
- # self.asr_pingping_timerout=0
- # self.AI_client_asr_ping()
- # 历史遗留:VC_Thinking_timerout 的重启通道会在 llm.created 后抢跑触发 AI_VC_start,
- # 直接打断本轮 LLM→TTS(notts 日志:llm.created 后立刻 AI_VC_start,导致无 tts.started)。
- # 现在统一由 _fsm_timeout_table_tick(Thinking 超时) 负责重启会话,因此禁用该旧通道。
- if self.VC_Thinking_timerout > 0 and self.VC_Thinking_timerout < 100:
- self.VC_Thinking_timerout = 100
- # def AI_kws_start(self):
- # print("AI_kws_start")
- # EventMesh.publish("wakeup_BT")
- # EventMesh.publish("system_sleep_rest")#防止黑屏
- # url="ws://118.145.158.207:22080/wakeup/ws"
- # self.AI_client_kws = uwebsocket.Client.connect(url, debug=True)
- # self.AI_client_VC_recv_id = _thread.start_new_thread(self.AI_client_VC_recv, (self.AI_client_VC,))
- # self.VC_Speech_Start()#开启线程获取录音数据
- #VC开始
- def AI_VC_start(self):
- print("AI_VC_start1")
-
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- self.VC_satrt_flag = 1
- self.VC_stop_play_audio_flag = 1
- self.VC_satrt_kws_flag = 1
- if self.VC_Thinking_timerout < 100:
- self.VC_Thinking_timerout = 0
- # 不在这里提前切灯态:播放/聆听灯只在“播放/录音真正开始/停止”处更新
- EventMesh.publish("wakeup_BT")
- #self.VC_Play_End()#关闭音频播放
- print("AI_VC_start2")
- #self.aud.aud_tone_play(16,200)#播放按键音
- self.LocalRespondingEnded_timerout = 0
- self.close_timerout = 0
- self._idle_deep_sec = 0
- self._vc_idle_deep_armed_suppress = 0
-
- #
- if self.AI_client_VC_isconnect == 0:#没连接的时候请求开始
- self.websocket_connect_VC()#连接服务器
-
- #utime.sleep_ms(50)
- else:
- print("AI_VC_start2 (already connected) {}".format(self._vc_fsm_log_snip()))
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- if self.AI_client_VC_isconnect == 0:
- #self.Close()
- return
- # 单对话 PTT:会话已 started 时勿重复发 Start(否则第二轮起上行/ASR 易乱)
- _ptt_skip_start = False
- try:
- if self.VC_mode == 1 and getattr(self, "_vc_session_started", 0) == 1:
- _ptt_skip_start = True
- print("[PTT] AI_VC_start skip duplicate Start (session active)")
- except Exception:
- pass
- if not _ptt_skip_start:
- self.AI_client_VC_start_requset()
- print("AI_VC_start3")
-
- if (not _ptt_skip_start) and self.AI_TTS_str == "" and self.AI_new_voice_url == "":
- self._uplink_request_start("AI_VC_start")
-
-
- # pass
- print("AI_VC_start4")
-
- #self.VC_Play_Start()
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
- #开始线程
-
- # if self.AI_VC_send_data_queue_thread_id == 0:
- # _thread.stack_size(self.Audio_buf_size+8*1024)
- # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())
- # _thread.stack_size(8*1024)
- # print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))
-
- if self.AI_audio_queue_thread_id ==0:
- _thread.stack_size(24*1024)
- self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())
- _thread.stack_size(8*1024)
- print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))
- else:
- print("AI_VC_start :AI_audio_queue_thread_id != 0 ")
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
- self._1S_timer.start(1000,1,self._1S_timer_cb)
- # self.VC_pingping_timerout = 0
- # self.asr_pingping_timerout = 0
- print("AI_VC_start5")
- #开始会话
- def AI_client_VC_start_requset(self):
- voice="zh_male_wennuanahu_moon_bigtts"
- try:
- voice = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["voice"]
- except Exception as e:
- usys.print_exception(e)
- #voice="zh_female_dongmanxiaoyang_moon_bigtts"
- token = EventMesh.publish("get_dev_token")
- if token ==None:
- token = ""
- # bot_option:优先从 default_config(usr 可覆盖)读取 VC_BOT_ID / VC_WORKFLOW_ID,允许空字符串
- _def_bot_id = "7462617881076351002"
- _def_workflow_id = "7462621757327196187"
- try:
- if "vc_config" in globals() and vc_config:
- _vc_bot_id = vc_config.get_attr("VC_BOT_ID", _def_bot_id)
- _vc_workflow_id = vc_config.get_attr("VC_WORKFLOW_ID", _def_workflow_id)
- if _vc_bot_id is None:
- _vc_bot_id = ""
- if _vc_workflow_id is None:
- _vc_workflow_id = ""
- else:
- _vc_bot_id, _vc_workflow_id = _def_bot_id, _def_workflow_id
- except Exception:
- _vc_bot_id, _vc_workflow_id = _def_bot_id, _def_workflow_id
- json_data = \
- {
- "header": {
- "event": "Start",
- "trace_id": self.get_trace_id(),
- },
- "payload": {
- "simple_response": True,#不返回llm过程文本
- "device_id": modem.getDevImei(),
-
- "user_agent": "AudioClient/1.0",
- "dialog_attributes": {
- #"provider": "doubao",
- # "provider": "coze",
-
- "cfg": {
- "disable_empty_prompt" : True,#拦截空字符请求大模型
- "custom_variables": {
- "name": self.bot_name,
- "devToken":token,
- },
- "greeting":{
- "enabled":False,
- "content":""
- },
- "openai_option":{
- },
-
- "bot_option": {
- "use_workflow": True,
- #"input_format": "text",
- "bot_id": _vc_bot_id,
- "workflow_id": _vc_workflow_id,
-
- }
- }
- },
- "outbound_attributes": {
- "provider": "volcengine",
- "cfg": {
- "format": "pcm",
- "sample_rate": self.VC_play_samplerate,
- "bit_rate": 16,
- "channel": 1,
- "volume": self.VC_TTS_volume,
- "emotion": self.emotion,
- #"voice": "7426720361732980745",
- "voice": voice,#音色
- "speech_rate": self.speech_rate,#语速
- "chunk_interval_ms": 10, # 配置每段音频入队间隔时间 100ms
- }
- },
- "inbound_attributes": {
- # "provider": "xun-fei",#
- "provider": "volcengine",
- "disable_vad":True,
- "vad_silence":self.vad_silence,
- "interrupt_options":{
- "concatenate_results":True,
- "thinking_time_only": True,
- #"keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断)
- #"interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表
- },
- "cfg": {
- "format": "pcm",
- "sample_rate": self.VC_record_samplerate,
- "bit_rate": 16,
- "channel": 1,
- "enable_punctuation": True,
- "enable_itn":True,
- "keywords":[]
- }
- },
- "commands":self.commands,
- }
- }
- # "bot_id":"7481164663863459850",#鸭子
- # "workflow_id":"7480485093845991443",
- # "bot_id":"7462617881076351002",#语言助手
- # "workflow_id":"7462621757327196187",
- # "bot_id":"7469964258592931852",#深圳百事通
- # "workflow_id":"7469968083063603241",
- # "bot_id":"7411071327182438412",#柒牌
- # "workflow_id":"7469049680291610636",
- # if self.online_kws == 1:#打开
- # json_data["payload"]["inbound_attributes"]["interrupt_options"]["keyword_interrupt"] = True
- # json_data["payload"]["inbound_attributes"]["interrupt_options"]["interrupt_keywords"] = ["你好,柒先生","你好"]
- #如果
- json_data["payload"]["outbound_attributes"]["provider"]=self.outbound_attributess_provider#TTS音色服务选择,火山
- json_data["payload"]["inbound_attributes"]["provider"]=self.inbound_attributess_provider#ASR服务选择,火山
- #优先选择self.VC_TTS_voice_type[self.VC_TTS_voice_key]里得参数
- # if "inbound_attributess_provider" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:
- # if len(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"])>0:
- # json_data["payload"]["inbound_attributes"]["provider"]=self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"]
- # print("inbound_attributess_provider是VC_TTS_voice_type里的:{}".format(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"]))
- if "outbound_attributess_provider" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:
- if len(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"])>0:
- json_data["payload"]["outbound_attributes"]["provider"]=self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"]
- print("outbound_attributess_provider是VC_TTS_voice_type里的:{}".format(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"]))
-
- # 根据配置设置默认 bot/workflow,并允许 VC_TTS_voice_type 覆盖
- try:
- if "vc_config" in globals() and vc_config:
- opt = vc_config.get_attr("DIALOG_BOT_OPTION", None)
- if isinstance(opt, dict):
- if "bot_id" in opt:
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["bot_id"] = opt.get("bot_id") or ""
- if "workflow_id" in opt:
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["workflow_id"] = opt.get("workflow_id") or ""
- if "use_workflow" in opt:
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["use_workflow"] = bool(opt.get("use_workflow"))
- except Exception:
- pass
-
- try:
- # voice 配置可覆盖 bot_id/workflow_id/openai_option
- cur = self.VC_TTS_voice_type[self.VC_TTS_voice_key]
- if isinstance(cur, dict):
- if cur.get("bot_id"):
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["bot_id"] = cur.get("bot_id")
- if "workflow_id" in cur:
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["workflow_id"] = cur.get("workflow_id") or ""
- if cur.get("openai_option"):
- json_data["payload"]["dialog_attributes"]["cfg"]["openai_option"] = cur.get("openai_option")
- except Exception:
- pass
-
- try:
- _bo = json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]
- if not _bo.get("bot_id", "") or not _bo.get("workflow_id", ""):
- _bo["use_workflow"] = False
- except Exception:
- pass
- try:
- if "vc_config" in globals() and vc_config:
- p = vc_config.get_attr("DIALOG_PROVIDER_DEFAULT", None)
- if p:
- json_data["payload"]["dialog_attributes"]["provider"] = p
- except Exception:
- pass
-
- try:
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEYWORDS_FROM_VOICE_TYPE", False):
- keywords = []
- for key in self.VC_TTS_voice_type:
- keywords.append(key)
- json_data["payload"]["inbound_attributes"]["cfg"]["keywords"] = keywords
- except Exception:
- pass
- if self.dialog_attributes_provider != None:
- json_data["payload"]["dialog_attributes"]["provider"] = self.dialog_attributes_provider
- if self.commands_mode !=0 :
- json_data["payload"]["commands"] = []
- if self.VC_mode == 0 :#and dev_type != "7PIN_BK_coze":#长对话模式打开vad
- json_data["payload"]["inbound_attributes"]["disable_vad"] = False
- # 新会话开始:在收到服务端 started 前,禁止发送音频帧(避免 session not initialized)
- self._vc_session_started = 0
- try:
- self.fsm.on_session_start_request()
- except Exception:
- pass
- 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(
- _tid or "-",
- self.VC_mode,
- json_data.get("payload", {}).get("inbound_attributes", {}).get("disable_vad", "?"),
- self.AI_client_VC_isconnect))
- except Exception:
- pass
-
- #开始上传语音
- def AI_client_VC_send_speech(self):
- json_data = \
- {
- "header": {
- "event": "audio.create",
-
- },
- }
-
- if self.VC_mode == 1:#单对话模式
- # 协议门控(可用 VC_PROTO_GUARDS 临时关闭)
- if (not VC_PROTO_GUARDS) or (self._vc_session_started == 1):
- self._ptt_audio_created = 1
- try:
- print("[VC][proto] audio.create send session_started={} proto_guards={}".format(
- self._vc_session_started, VC_PROTO_GUARDS))
- except Exception:
- pass
- self.AI_client_VC_send(ujson.dumps(json_data))
- else:
- try:
- print("[VC][proto] audio.create SKIP session_started={} VC_PROTO_GUARDS={} "
- "(等对端 started 后再开麦)".format(self._vc_session_started, VC_PROTO_GUARDS))
- except Exception:
- pass
-
-
-
-
-
-
- try:
- self.fsm.on_server_asr_started()
- except Exception:
- pass
- #结束上传语音
- def AI_client_VC_end_speech(self):
- json_data = \
- {
- "header": {
- "event": "audio.completed",
-
- },
- }
- self.AI_client_VC_send(ujson.dumps(json_data))
- self._ptt_audio_created = 0
- EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"audio.completed"})
-
-
- #客户端打断对话
- def AI_client_VC_RequestToSpeak(self):
- json_data = \
- {
- "event": "cancel",
- "payload": {}
- }
- print("AI_client_VC_RequestToSpeak!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- # 模式切换等场景 play_tts(..., is_close_VC=1) 会置 Close_flag=1;用户再按 AI 键走 cancel→canceled→
- # uplink_start 时,FSM 与 _uplink_request_start 均会因 Close_flag 拒绝开麦,表现为“切回长对话后按键无上行”。
- try:
- if self.VC_mode == 0:
- self.Close_flag = 0
- self._vc_pending_interrupt_uplink = 1
- self._vc_clear_stale_record_state("RequestToSpeak")
- except Exception:
- pass
- # 只做“打断+清播放队列”,不要在这里关音频队列线程(会导致后续 TTS/提示音不稳定,且触发竞态)
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_init})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_send,"data":ujson.dumps(json_data)})
- utime.sleep_ms(1)
- try:
- self.fsm.reset_rx_phases()
- except Exception:
- pass
- # #self.Close()
- # self.AI_VC_start()
- #TTS播放
- def AI_client_VC_synthesis(self):
- # 幂等保护:在上一条 synthesis 还未等到 tts.started 前,不重复发(防止多兜底/多入口重复触发)
- try:
- if self._tts_pending == 1:
- return
- except Exception:
- pass
- json_data = \
- {
- "event": "synthesis",
- "payload": {
- "text": self.AI_TTS_str
- }
- }
- # 语义标记只用于“是否自动开麦”的策略。
- # 当前策略:长对话不自动开麦(需按键开始),因此这里不对提示音做自动开麦标记。
- self.AI_TTS_str = ""
- # 标记为“等待 tts.started”,避免在 synthesis→tts.started 窗口误开下一轮录音上行
- self._tts_pending = 1
- try:
- self._tts_pending_ms = utime.ticks_ms()
- except Exception:
- self._tts_pending_ms = 0
- try:
- self.fsm.on_local_synthesis_sent()
- except Exception:
- pass
- print("AI_client_VC_synthesis!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- # if self.VC_state == VC_State_RequestToRespond:
- # print("AI_client_VC_RequestToRespond 重复 tts_str:{}\nasr_str:{}".format(self.AI_TTS_str,self.AI_ASR_str))
- self.AI_client_VC_send(ujson.dumps(json_data))
- # 金三 V2.5:AudioPlayed — 端侧 TTS 播报结束后上报,服务端进入下一轮对话(轻度重同步,不断开 WebSocket)
- def AI_client_VC_audio_played(self):
- # V2.5:播完上报;与 VC_PROTO_GUARDS 无关(见文件头说明)
- try:
- if self.AI_client_VC_isconnect != 1 or self.AI_client_VC == None:
- return
- json_data = {"event": "audio.played", "payload": {}}
- print("AI_client_VC_audio_played")
- self.AI_client_VC_send(ujson.dumps(json_data))
- except Exception as e:
- usys.print_exception(e)
- def kws_even(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.kws_even_cb})
- def kws_even_cb(self):
- if EventMesh.publish("get_sos_flag") == 1:
- return
- print("kws_even_cb")
- if self.VC_mode == 1:
- try:
- tts_str = vc_config.get_attr("KWS_PTT_HINT", "请您按住圆形的按钮开始讲话,讲完松开即可 ")
- except Exception:
- tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "
- EventMesh.publish("play_tts",[tts_str,1])
- return
- if self.VC_mode == 0:
- if EventMesh.publish("get_voice_record_flag") == 1:#停止静默录音
- if EventMesh.publish("get_voice_record_mode") == 2:#语音留言不能被打断
- return
- elif EventMesh.publish("get_voice_record_mode") == 1: #静默录音需要打断
- EventMesh.publish("stop_voice_record")#停止静默录音
- self.aud.aud_tone_play(10,400)
- # EventMesh.publish("audio_montage_play",["chenggong.mp3"])
- self.Close()
- # if dev_type == "7PIN" :
- # self.aud.aud_tone_play(10,400)#播放按键音
- #self.aud.aud_tone_play(10,200)#播放按键音
- self.AI_TTS_str = ""#"你好,有什么吩咐"
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- self._uplink_request_stop("kws_even_cb")
- self._uplink_request_start("kws_even_cb")
- self.AI_VC_start()
-
- def set_vad_silence(self,event = None,msg= None):
- self.vad_silence = msg
- EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg})
- #设置tts参数
- def set_wait_first_flag(self,event = None,msg= None):#设置未读语音播报初次记录flag
- self.wait_first_flag = msg
- def set_VC_TTS_voice_type(self,event = None,msg= None):
- self.VC_TTS_voice_type["APP_voice"] = msg
- #self.VC_TTS_voice_type[self.VC_TTS_voice_key]
- self.VC_TTS_voice_key = "APP_voice"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- print("[VC] TTS_voice key={} map={}".format(self.VC_TTS_voice_key, self.VC_TTS_voice_type))
- #修改音量大小 msg为1则为加大,msg为0则为减小,返回1成功,返回0失败
- def change_volume(self,event =None, msg =None):
- if msg == 0 :
- self.VC_TTS_volume -= 10
- if self.VC_TTS_volume < 0:
- self.VC_TTS_volume = 0
- return 0
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- return 1
- elif msg == 1:
- self.VC_TTS_volume += 10
- if self.VC_TTS_volume > 100:
- self.VC_TTS_volume = 100
- return 0
- else:
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- return 1
- elif msg >= 100:#修改为具体音量
- if msg > 200:
- self.VC_TTS_volume = 100
- return 0
- else:
- self.VC_TTS_volume = msg - 100#还原数据
- self.aud.setVolume(self.VC_TTS_volume //10)
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
- EventMesh.publish("post_device_state")
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
- return 1
- #调节聆听快慢(以秒为单位)
- def NLP_set_vad_silence(self,event,msg):
- if msg == 1:#聆听变快
- self.vad_silence -= 1000
- if self.vad_silence < 1000:
- AI_TTS_str = "已经是最快了"
- self.vad_silence = 1000
- else:
- AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000)
- elif msg == 0:#聆听变慢
- self.vad_silence += 1000
- if self.vad_silence > 4000:
- self.vad_silence = 4000
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
- AI_TTS_str = "已经是最慢了"
- else:
- AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000)
- EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])
- return AI_TTS_str
- #查询指令判断
- def find_cmd(self,event ,msg):
- if "电量" in msg:
- battery = EventMesh.publish("get_battery")
- print("电量查询:{}".format(battery))
- tts_play_str = "电量剩余百分之{}".format(battery)
- EventMesh.publish("play_tts",[tts_play_str,0])
- # print("电量!!!!!!!!!!!")
- elif "信号" in msg:
- signal = net.csqQueryPoll()
- tts_play_str = "当前信号{}".format(signal)
- EventMesh.publish("play_tts",[tts_play_str,0])
- # print("信号!!!!!!!!!!!")
- elif "卡号" in msg:
- # print("卡号!!!!!!!!!!!")
- iccid = sim.getIccid()
- iccid = num_str_to_chinese(iccid)#转为中文
- if iccid != -1 and len(iccid) > 5:
- print("iccid{}".format(iccid))
- tts_play_str = "当前卡号为:{}".format(iccid)
- else:
- tts_play_str = "卡号查询失败"
- EventMesh.publish("play_tts",[tts_play_str,0])
- elif "设备号" in msg:
- imei = modem.getDevImei()
- if imei != -1 and len(imei) > 5:
- print("IMEI:{}".format(imei))
- tts_play_str = "当前设备号为:{}".format(imei)
- else:
- tts_play_str = "设备号查询失败"
- EventMesh.publish("play_tts",[tts_play_str,0])
- elif "音量" in msg :
- tts_play_str = "当前音量是{}".format(self.VC_TTS_volume)
- EventMesh.publish("play_tts",[tts_play_str,0])
- elif "语速" in msg :
- tts_play_str = "当前语速是{}".format(EventMesh.publish("find_rate"))
- EventMesh.publish("play_tts",[tts_play_str,0])
-
- #改变音色判断
- def change_voice(self,event ,msg):
- voice = msg
- # if "男声" in msg or "普通话" in msg:
- # self.VC_TTS_voice_key = "男"
- # else:
- # self.VC_TTS_voice_key = voice
- self.outbound_attributess_provider = "xun-fei"
- if "男声" in msg or "普通话" in msg:
- self.VC_TTS_voice_key = "男"
- self.outbound_attributess_provider = "volcengine"
- elif "女声" in msg:
- self.VC_TTS_voice_key = "女"
- self.outbound_attributess_provider = "volcengine"
- elif "童声" in msg:
- self.VC_TTS_voice_key = "童"
- self.outbound_attributess_provider = "volcengine"
- elif "香港粤语" in msg:
- self.VC_TTS_voice_key = "香港粤语"
- elif "四川话" in msg:
- self.VC_TTS_voice_key = "四川"
- elif "上海话" in msg:
- self.VC_TTS_voice_key = "上海"
- elif "安徽合肥话" in msg:
- self.VC_TTS_voice_key = "安徽合肥"
- elif "山东话" in msg:
- self.VC_TTS_voice_key = "山东话"
- elif "陕西话" in msg:
- self.VC_TTS_voice_key = "陕西"
- elif "广东话" in msg:
- self.VC_TTS_voice_key = "广东"
- elif "成都话" in msg:
- self.VC_TTS_voice_key = "成都"
- elif "湖南话" in msg:
- self.VC_TTS_voice_key = "湖南"
- elif "苏州话" in msg:
- self.VC_TTS_voice_key = "苏州"
- elif "东北话" in msg:
- self.VC_TTS_voice_key = "东北"
- elif "河南话" in msg:
- self.VC_TTS_voice_key = "河南"
- elif "湖北话" in msg:
- self.VC_TTS_voice_key = "湖北"
- elif "闽南话" in msg:
- self.VC_TTS_voice_key = "闽南"
- elif "内蒙古话" in msg:
- self.VC_TTS_voice_key = "内蒙古话"
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- tts_play_str = "声音已改为{}".format(voice)
- EventMesh.publish("play_tts",[tts_play_str,0])
-
- #闹钟的url播放接口
- def play_single_voice(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_single_voice_cb,"data":msg})
- def play_single_voice_cb(self,data):
- try:
- new_voice_url = data[0] #音频地址
- is_close_VC = data[1]
- if new_voice_url != "":
- self.AI_new_voice_url = new_voice_url
- self.alarm_play_flag = 1
- if is_close_VC != None:
- self.Close_flag = is_close_VC
- 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()
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_single_voice_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
- self.play_new_voice_flag = 1
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- pass
- #语音留言
- def play_new_voice(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg})
- def play_new_voice_cb(self,data):
- try:
- new_voice_url = data[0] #音频地址
- is_close_VC = data[1]
- if new_voice_url != "":
- self.recv_voice = 1#收到留言
- print("已收到最新留言!!!!!")
- # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数
- self.AI_new_voice_url = new_voice_url
- # if self.pwk_cnt_voice == 0 and self.pwk_notice_flag == 0:#并且不在多条留言的状态
- # EventMesh.publish("play_tts",["您有一条语音留言,10秒内按下中间键可以收听",1])
- if self.pwk_notice_flag == 0 and EventMesh.publish("get_app_new_voice_flag"):#不处于播放状态并且是小程序下发的语音而不是请求的
- self.pwk_cnt_voice += 1#加上当前这条
- EventMesh.publish("set_storage_config",["is_noread_voice",1])
- EventMesh.publish("set_app_new_voice_flag",0)
- print("voice_add_to = {}".format(self.pwk_cnt_voice))
- print("self.pwk_notice_flag = {}".format(self.pwk_notice_flag))
- if self.pwk_cnt_voice >1:
- if self.pwk_notice_flag == 0:
- tts_str = "您有" + str(self.pwk_cnt_voice) + "条未读语音留言,10秒内按下中间键可以收听"
- EventMesh.publish("play_tts",[tts_str,1])
- elif self.pwk_cnt_voice == 1:
- if self.pwk_notice_flag == 0:
- tts_str = "您有1条语音留言,10秒内按下中间键可以收听"
- EventMesh.publish("play_tts",[tts_str,1])
- if is_close_VC != None:
- self.Close_flag = is_close_VC
- 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()
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_new_voice_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
- self.play_new_voice_flag = 1
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- pass
- #历史留言查看
- def play_his_voice(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_his_voice_cb,"data":msg})
- def play_his_voice_cb(self,data):
- try:
- new_voice_url = data[0] #音频地址
- is_close_VC = data[1]
- time = data[2]
- tts_play_str = time + "收到"
- EventMesh.publish("play_tts",[tts_play_str,1])
- if new_voice_url != "":
- self.recv_voice = 1#收到留言
- print("历史留言查看")
- # tts_play_str = "收到"
- # EventMesh.publish("play_tts",[tts_play_str,1])
- # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数
- self.AI_new_voice_url = new_voice_url
- if is_close_VC != None:
- self.Close_flag = is_close_VC
- 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()
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_his_voice_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
- self.play_new_voice_flag = 1
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- pass
- #等待未读留言查看
- def play_wait_voice(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_wait_voice_cb,"data":msg})
- def play_wait_voice_cb(self,data):
- try:
- new_voice_url = data[0] #音频地址
- is_close_VC = data[1]
- print("是否为第一次:{}".format(self.wait_first_flag))
- if self.wait_first_flag == 0:
- tts_play_str = "您有" + str(len(EventMesh.publish("get_play_new_voice_list"))) + "条未听留言,10秒内按下中间键可以收听"
- EventMesh.publish("play_tts",[tts_play_str,1])
- # self.wait_first_timer.start(1000,1,self.noread_wait_vocie)
- self.wait_first_flag = 1
- elif self.wait_first_flag == 1:
- self.wait_sort += 1
- self.AI_TTS_str = "第"+str(self.wait_sort)+"条"
- if new_voice_url != "":
- self.recv_voice = 1#收到留言
- print("未读留言查看")
- self.AI_new_voice_url = new_voice_url
- if is_close_VC != None:
- self.Close_flag = is_close_VC
- 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()
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_wait_voice_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
- self.play_new_voice_flag = 1
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- pass
- def noread_wait_vocie(self,*args):#如果一直没读
- self.wait_first_cnt += 1
- print("未读留言等待收听中:{}".format(self.wait_first_cnt))
- if EventMesh.publish("get_recv_voice") == 1:#如果检测到留言播放
- self.wait_first_cnt = 0
- self.wait_first_timer.stop()
- if self.wait_first_cnt == 15:
- EventMesh.publish("set_wait_first_flag",0)
- EventMesh.publish("set_history_voice_flag",0)
- EventMesh.publish("set_wait_voice_flag",0)
- self.wait_first_cnt = 0
- self.wait_first_timer.stop()
- #音乐播放
- def play_music(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_music_cb,"data":msg})
- def play_music_cb(self,data):
- try:
- music_name = data[0]
- new_voice_url = data[1] #音频地址
- is_close_VC = data[2]
- if new_voice_url != "":
- self.AI_new_voice_url = new_voice_url
- self.music_play_flag = 1#表示音乐播放开启
- EventMesh.publish("play_tts",["下面为您播放歌曲{}".format(music_name),1])
- if is_close_VC != None:
- self.Close_flag = is_close_VC
- 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()
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_music_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
- self.play_new_voice_flag = 1
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
-
- #AI翻译接收TTS
- def play_trans_tts(self,event = None,msg= None):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_trans_tts_cb,"data":msg})
- def play_trans_tts_cb(self,data):
- try:
- new_voice_url = data #音频地址
- if new_voice_url != "":
- self.AI_new_voice_url = new_voice_url
- 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()
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})
- self.AI_ASR_send_buf_len = 0
- self._uplink_request_stop("play_trans_tts_cb")
- self.websocket_deconnect_VC()
- self.AI_VC_start()
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
- self.play_new_voice_flag = 1
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- except Exception as e:
- usys.print_exception(e)
- def AI_client_new_voice(self): #上传未读的语音地址/包括音频地址也可以
- json_data = \
- {
- "event": "audio.convert",
- "payload": {
- "audio_convert_request": {
- "audio_url": self.AI_new_voice_url,
- "bit_depth": 16,
- "channels": 1,
- "sample_rate": 16000,
- "target_format": "pcm"
- }
- }
- }
- self.AI_new_voice_url = ""
- print("AI_client_new_voice!!!!!!!!!!!!!!!!!!!!!!!!!!!!!{}".format(json_data))
- self.AI_client_VC_send(ujson.dumps(json_data))
-
-
- def chenggong_tone(self):
- EventMesh.publish("audio_montage_play",["chenggong.mp3"])
- # self.aud.aud_tone_play(10,400)
- def send_tone(self):
- EventMesh.publish("audio_montage_play",["send_tone.mp3"])
- def concurrency_limit_exceeded_tone(self):
- self.Close()
- EventMesh.publish("audio_montage_play",["chaobingfatis.mp3"]) #播放超并发提醒
- #给服务器回应ack
-
-
- ############################################
- #关闭音频播放线程
- def AI_audio_queue_thread_close(self):
- print("AI_audio_queue_thread_close1!!!!!!!!!!!!!!!!!!!!!!")
- if self.AI_audio_queue_thread_id != 0:
- count =500
- while True:
- count -= 1
- if self.AI_audio_queue_thread_lock == 0 or count <=0:
- break
- else:
- self.AI_audio_queue_thread_lock = 2
- print("AI_audio_queue_thread going:{}...".format(count))
- utime.sleep_ms(1)
- self.AI_audio_queue_thread_lock = 0
- print("AI_audio_queue_thread_close2!!!!!!!!!!!!!!!!!")
- if self.AI_audio_queue_thread_id != 0:
- try:
- _thread.stop_thread(self.AI_audio_queue_thread_id)
- except Exception as e:
- usys.print_exception(e)
- self.AI_audio_queue_thread_id = 0
- # 音频队列
- def AI_audio_queue_thread(self):
- print("AI_audio_queue_thread start")
- count = 0
- if self.AI_audio_queue.empty()!=True:
- self.AI_audio_queue.clear()
- print("音频数据还有剩余!!!!!!!!!!!!")
- while True:
- try:
- if self.AI_audio_queue_thread_lock == 2 or self.VC_stop_play_audio_flag == 0:
- print("AI_audio_queue_thread 该退出了")
- break
- #
- count+=1
- if count%500 == 1 and VC_HEARTBEAT_LOG:
- print("AI_audio_queue_thread on")
- utime.sleep_ms(1)
- #print("AI_audio_queue_thread")
- if self.AI_audio_queue.empty() :
- self.AI_audio_queue_thread_lock = 0
-
-
- else:
- self.AI_audio_queue_thread_lock = 1
- data = self.AI_audio_queue.get()
- self.close_timerout = 0
- if self.play_flag == 0:
- self.play_flag =1
- #self.AI_client_tts_data_len=len(data)
- #print("audio_queue:len:{}".format(len(data[0])))
-
- if len(data[0]) > 10:
- try:
- if self._ptt_should_drop_inbound_tts():
- continue
- except Exception:
- pass
- for i in range(5):
- if self.VC_Play_flag == 0:
- self.VC_Play_Start()
- utime.sleep_ms(2)
- else:
- break
- # 打断后 PCM 偶发未打开:加重试,避免「队列有数据、play_pcm=None、无声」
- if self.VC_play_TYPE == VC_play_TYPE_PCM and self.play_pcm is None and self.VC_stop_play_audio_flag == 1:
- for j in range(4):
- if self.VC_Play_flag == 0:
- self.VC_Play_Start()
- utime.sleep_ms(30 + j * 35)
- if self.play_pcm is not None:
- break
- try:
- if self.VC_play_TYPE == VC_play_TYPE_PCM:
- # if self.VC_play_samplerate == 8000:
- # tmp= self.aud.playStream(1,data[0] )
- # else:
- write_buf = bytearray(data[0])
- #print("play_pcm.stream_write1 len:{}".format(len(write_buf)))
- if self.play_pcm != None:
- #print("play_pcm.stream_write !!!")
- ret = self.play_pcm.stream_write(write_buf,len(write_buf))
- #ret= self.aud.playStream(1, data[0])#播放音频流
- if self.tts_out_data_len == 0:
- try:
- sid = getattr(self, "_cur_sid", "")
- did = getattr(self, "_cur_dialog_id", "")
- except Exception:
- sid, did = "", ""
- print("[TTS] first_write ret={} len={} sid={} did={}".format(
- ret, len(write_buf), sid, did
- ))
- if ret <= 0:
- print("[TTS] stream_write ret={} len={} (no audio?)".format(ret, len(write_buf)))
- self.tts_out_data_len += ret
- #print("play_pcm.stream_write2:{}".format(self.tts_out_data_len))
- elif self.VC_play_TYPE == VC_play_TYPE_MP3:
- tmp= self.aud.playStream(3, data[0])#播放音频流
-
- if tmp != 0:
- print("2playStream:error tmp:{} data len:{}!!!!!!!!!!!!".format(tmp,len(data[0])))
- # # #播放失败的时候填充WAV头
- # data_len = len(data[0])
- # data_len_b = data_len.to_bytes(4,'little')#数据长度转16进制数组
- # h_data = [ 0x52 ,0x49 ,0x46 ,0x46 #,0x24 ,0x00 ,0x00 ,0x00 #文件标识、文件大小
- # ,0x57 ,0x41 ,0x56 ,0x45 ,0x66 ,0x6D ,0x74 ,0x20 #文件格式、格式块标识符
- # ,0x10 ,0x00 ,0x00 ,0x00 ,0x01 ,0x00 ,0x01 ,0x00 ,0x80 ,0x3E ,0x00 ,0x00 #格式块大小4、音频格式2、声道数2、采样率4
- # ,0x80 ,0x3E ,0x00 ,0x00 ,0x02 ,0x00 ,0x10 ,0x00 ,0x64 ,0x61 ,0x74 ,0x61]#每秒字节数4、数据块对齐2、位深度2、data4
- # h_data_b = bytes(h_data)
- # file_len = data_len+len(h_data_b)+4+4
- # file_len_b = file_len.to_bytes(4,'little')#文件长度转16进制数组
- # h_data_b = h_data_b[:4]+file_len_b+h_data_b[4:]+data_len_b
- # #self.aud.playStream(2, h_data_b)#播放音频流
- # tmp= self.aud.playStream(2, h_data_b+data[0])#播放音频流
- # print("3playStream:tmp:{} !!!!!!!!!!!!".format(tmp))
-
-
- mem = gc.mem_free()
- #print('1剩余内存RAM:{} KB'.format(mem / 1024))
- except Exception as e:
- print("AI_audio_queue_thread:ERROR:")
- usys.print_exception(e)
-
- # if self.VC_state == VC_State_TTSEnd and self.AI_audio_queue.empty() :#self.AI_audio_queue为空且tts下发结束
- # #通知服务器播放结束
- # self.AI_client_VC_LocalRespondingEnded()
- except Exception as e:
- print("AI_audio_queue_thread:ERROR:")
- usys.print_exception(e)
- self.AI_audio_queue_thread_lock = 0
- self.AI_audio_queue_thread_id = 0
- print("AI_audio_queue_thread:exit")
- def factory_mode(self):
- print("factory_mode")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.factory_start})
- def factory_start(self):
- self.factory_timer.start(3000, 1, self.factory_timer_cb)
- def factory_timer_cb(self, *args, **kwargs):
- print("factory_timer_cb play......")
- #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
- EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])
- def Close_VC(self,event,msg):
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- def Close(self):
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] Close() enter VC_mode={} {} VC_satrt_flag={} VC_record_state_flag={}".format(
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(),
- getattr(self, "VC_satrt_flag", -1), getattr(self, "VC_record_state_flag", -1)))
- except Exception:
- pass
- self.VC_stop_play_audio_flag = 0
- try:
- self.AI_key_long_press_flag = 0
- except Exception:
- pass
- self.VC_Thinking_timerout = 0
- self.AI_ASR_timer.stop()
- self._1S_timer.stop()
- self.close_timerout = 0
- self._idle_deep_sec = 0
- self.factory_timer.stop()
- self.AI_TTS_str = ""
- self.AI_ASR_str = ""
- self.AI_ASR_str_tmp = ""
- self.AI_ASR_flag = 0
- self.AI_ASR_check_timeout = 0
- self.AI_ASR_error_timeout = 0
- self.LocalRespondingEnded_timerout = 0
- self._need_audio_played_ack = 0
- self._tts_pending = 0
- self._tts_pending_ms = 0
- self._vc_session_started = 0
- self._ptt_audio_created = 0
- self._vc_pending_interrupt_uplink = 0
- try:
- self._skip_started_uplink_once = 0
- except Exception:
- pass
- try:
- self._ptt_abort_interrupt_arm()
- except Exception:
- pass
- # Power 短按停止对话后,再次按 AI 需要新建 dialog_id:清理本轮 dialog/session 标识
- try:
- self._cur_dialog_id = ""
- except Exception:
- pass
- try:
- self._cur_sid = ""
- except Exception:
- pass
- self.is_ruppt_tts = 0#打断标志位清零
- print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] step=set_led_state_idle (first)")
- except Exception:
- pass
- EventMesh.publish("set_led_state", {"ai_state": "idle"})
- self.tts_in_data_len = 0
- self.tts_out_data_len = 0
- #必须先销毁线程,不然线程会容易报错
-
- try:
- self.AI_audio_queue_thread_close()#销毁音频线程
- except Exception as e:
- usys.print_exception(e)
- try:
- self.AI_VC_send_data_queue_thread_close()#销毁数据发送线程
- except Exception as e:
- usys.print_exception(e)
-
- # Close 可能被不同线程触发:统一切回主线程执行 stop
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] step=schedule_uplink_stop reason=Close (async publish_AI_btnInfo)")
- except Exception:
- pass
- EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"Close"})
-
- print("ai_VC Close1.1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-
- print("ai_VC Close1.2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] step=websocket_deconnect_VC")
- except Exception:
- pass
- try:
- self.websocket_deconnect_VC()
- except Exception as e:
- usys.print_exception(e)
-
-
-
-
-
- print("ai_VC Close1.3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-
- print("ai_VC Close2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-
-
-
-
- if self.record_data_save_flag == 1:
- if self.record_fd :
- self.record_fd.close()
- self.record_fd = None
- if self.play_data_save_flag == 1:
- if self.play_fd !=None:
- self.play_fd.close()
- self.play_fd = None
- print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")
-
- if self.AI_VC_send_data_queue.empty()!=True:
- self.AI_VC_send_data_queue.clear()
- self.play_new_voice_flag = 0 #语音消息播放状态
- if self.voice_choose_flag == 0: # 如果没有未读语音
- self.AI_new_voice_url = "" # 语音播放地址
- print("关闭并重置语音标志位")
- else:
- print("关闭但是没有重置语音标志位")
- self.AI_ASR_send_buf_len = 0
- #self.AI_WS_connect_Queue.put(self.AI_client_VC)
- try:
- if getattr(self, "VC_mode", 0) != 1:
- print("[VC][long_idle] step=set_led_state_idle (second) -> VC_init VC_satrt_flag=0")
- except Exception:
- pass
- EventMesh.publish("set_led_state", {"ai_state": "idle"})
- self.VC_init()
- self.VC_satrt_flag = 0
- # Level2:会话结束后仍可继续 1s 节拍累计 idle 直至深睡(依赖 VC_IDLE_DEEP_SLEEP_SEC)
- try:
- _dsec = 60
- _den = 1
- if "vc_config" in globals() and vc_config:
- _dsec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))
- _den = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))
- if _den and _dsec > 0:
- self._1S_timer.start(1000, 1, self._1S_timer_cb)
- except Exception:
- pass
- # try:
- # while True:
- # if self.AI_WS_connect_Queue.empty()!=True:
- # AI_WS_connect = self.AI_WS_connect_Queue.get()
- # if AI_WS_connect :
- # try:
- # AI_WS_connect._close()
- # AI_WS_connect = None
- # except Exception as e:
- # usys.print_exception(e)
- # else:
- # print("websocket链接已为空")
- # else:
- # break
- # except Exception as e:
- # usys.print_exception(e)
-
- def VC_init2(self):
- self.VC_init()
-
- print("ai_VC VC_init2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- try:
- self.websocket_deconnect_VC()
- except Exception as e:
- usys.print_exception(e)
- print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")
- self._1S_timer.stop()
- if self.AI_VC_send_data_queue.empty()!=True:
- self.AI_VC_send_data_queue.clear()
-
- self.AI_ASR_send_buf_len = 0
- if self.AI_audio_queue.empty()!=True:
- self.AI_audio_queue.clear()
- def VC_init(self):
- _q0 = self.AI_audio_queue.size()
- self.VC_Play_End()
- if self.AI_audio_queue.empty()!=True:
- self.AI_audio_queue.clear()
- print("[VC] init audio_queue {} -> {}".format(_q0, self.AI_audio_queue.size()))
- # num = EventMesh.publish("get_volum_size")
- # if num == None:
- # num = 8
- # self.aud.setVolume(num)#恢复原始音量
-
-def num_str_to_chinese(num_str):
- # 数字与中文映射(含零)
- num_map = {
- '0': '零', '1': '一', '2': '二', '3': '三', '4': '四',
- '5': '五', '6': '六', '7': '七', '8': '八', '9': '九'
- }
- chinese = ''
- # 逐位转换,拼接单位
- for char in num_str:
- if char in num_map:
- digit = num_map[char]
- chinese += digit
- else:
- chinese += char
-
- return chinese
-
+from common import Abstract, LogAdapter\r
+try:\r
+ from qp_log import qp_print as print\r
+except Exception:\r
+ pass\r
+import EventMesh\r
+import uwebsocket\r
+import usys\r
+import gc\r
+import urandom\r
+import uhashlib\r
+import ubinascii\r
+import ujson\r
+import osTimer\r
+import _thread\r
+from queue import Queue\r
+import utime\r
+import audio\r
+import uos\r
+from machine import Pin\r
+from machine import ExtInt\r
+from machine import WDT\r
+import modem\r
+import sim\r
+import net\r
+import base64\r
+import ucryptolib\r
+import urandom as random\r
+import request\r
+import ql_fs\r
+from misc import Power,USB,PowerKey\r
+import ntptime\r
+import machine\r
+import dataCall\r
+import constants\r
+import versions\r
+import version_util\r
+from machine import UART\r
+try:\r
+ import vc_config\r
+except Exception as e:\r
+ usys.print_exception(e)\r
+try:\r
+ import vc_text_intent\r
+except Exception:\r
+ vc_text_intent = None\r
+try:\r
+ import VAD\r
+except Exception as e:\r
+ usys.print_exception(e)\r
+try: \r
+ import usr.ai_alarm as _ai_alarm\r
+except Exception as e:\r
+ usys.print_exception(e)\r
+ import ai_alarm as _ai_alarm\r
+PRESS_AUDIO_MODE = 0 #是按键音 0 是使用音频文件\r
+# dev_type = "G10"\r
+#dev_type = "7PIN_BK"#小瑞\r
+#dev_type = "7PIN_BK_coze"#k扣子小瑞\r
+#dev_type = "7PIN_BK_EU"#英文小瑞\r
+#dev_type = "7PIN_coze"#柒牌扣子\r
+dev_type = "7PIN"#柒牌(默认值,实际优先从 usr/default_config.py 覆盖)\r
+# dev_type = "7PIN_1_5"#柒牌1.5\r
+#dev_type = 'momo'#食梦貘\r
+#dev_type = 'sjgq'#四季甘泉\r
+#dev_type = 'szbst'#深圳百事通\r
+#dev_type = 'jswm'#金三完美\r
+#dev_type = 'jswm_BOX'#金三完美柒牌盒子\r
+#dev_type = 'csh'#慈善会\r
+\r
+# 统一从配置加载 DEV_TYPE(usr 覆盖优先)\r
+try:\r
+ if "vc_config" in globals() and vc_config:\r
+ dev_type = vc_config.get_dev_type(dev_type)\r
+except Exception:\r
+ pass\r
+\r
+# 1=打印 VC 周期性保活专用行「VC: ping ping」;默认 0(WS send 明细里已跳过 ping,无需再开此项)\r
+VC_HEARTBEAT_LOG = 0\r
+# 1=打印音频帧级日志:Record_len / send_data len / 下发音频长度;默认 0(只看 uplink START/END)\r
+# VC_AUDIO_FRAME_LOG = 0\r
+# 1=启用我们新增的协议层“严格门控/缓冲/兜底”行为;0=临时排除这些改动(用于定位协议影响)\r
+# 受控项(与对端 V2.5+ 能力对齐;关=回退旧端行为):\r
+# - 上行:收到 started 前缓冲/丢弃 PCM;PTT 模式下 audio.create 前禁发 PCM\r
+# - 兜底:llm.completed 后若久无 tts.started,端侧可重启会话(不做 force_tts)\r
+# 注意:audio.played 不受此开关控制——金三 V2.5 服务端依赖端侧播完后上报才能进入下一轮 ASR;\r
+# 若误关会导致问候结束后首轮用户说话无 vad/asr。\r
+VC_PROTO_GUARDS = 0\r
+# 1=打印 VC WebSocket 每条收发的完整文本(流量大、易刷屏);0 仅一行 event 摘要(llm.delta 省略)\r
+VC_WS_IO_LOG = 0\r
+# 长对话「无业务下行」idle:下列 JSON event/type 及裸文本 ping/pong 不重置 close_timerout。\r
+# 帧级 WebSocket PING/PONG 在 uwebsocket.recv() 内已处理,不会进入 AI_client_VC_recv。\r
+# 金三 V2.5 文档示例:WebSocket 文本帧 "ping" 保活;对端可能回 pong / heartbeat 类事件。\r
+_VC_KEEPALIVE_RX_EVENTS = frozenset(("pong", "ping", "heartbeat", "keepalive"))\r
+\r
+\r
+def _vc_out_json_event_meta(s):\r
+ """解析 WS 出站 JSON:event 可能在 header 或顶层(Start/audio.create vs synthesis)。"""\r
+ try:\r
+ jd = ujson.loads(s)\r
+ h = jd.get("header")\r
+ if isinstance(h, dict):\r
+ ev = h.get("event") or ""\r
+ if ev:\r
+ return ev, h.get("trace_id") or ""\r
+ ev = jd.get("event") or ""\r
+ return ev, ""\r
+ except Exception:\r
+ return "", ""\r
+\r
+\r
+class AI_led(Abstract):\r
+ def __init__(self):\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ r_gpio, g_gpio, b_gpio = vc_config.led_gpio_tuple()\r
+ else:\r
+ r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO21, Pin.GPIO19\r
+ except Exception:\r
+ r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO21, Pin.GPIO19\r
+ self._led_gpio_r = r_gpio\r
+ self._led_gpio_g = g_gpio\r
+ self._led_gpio_b = b_gpio\r
+ self.led_red = Pin(r_gpio, Pin.OUT, Pin.PULL_PD, 0)\r
+ self.led_green = Pin(g_gpio, Pin.OUT, Pin.PULL_PD, 1)\r
+ self.led_blue = Pin(b_gpio, Pin.OUT, Pin.PULL_PD, 1)\r
+ print("AI_led GPIO dev_type={} red={} green={} blue={}".format(\r
+ dev_type, r_gpio, g_gpio, b_gpio))\r
+\r
+ # 统一输入状态(充电/电量/信号/AI对话),按优先级表输出\r
+ self._st_ai = "idle" # idle/listening/thinking/playing\r
+ self._st_charging = "none" # none/charging/full\r
+ self._st_battery = "normal" # normal/low/empty\r
+ self._st_signal = "normal" # normal/weak\r
+ self._st_alert = None # None / rb_flash_2(REQ-ERR-01/03)\r
+\r
+ self._timer = osTimer()\r
+ self._mode = "off" # off/red_on/green_on/green_blink/blue_blink/red_blink/bg_alt/alert_rb\r
+ self._alert_steps_left = 0\r
+ self._alert_rb_phase = 0\r
+ self._alert_period_ms = 250\r
+ self._blink_on = 0\r
+ self._bg_phase = 0\r
+ self._period_ms = 1000\r
+ self._gb_on_ms = 1000\r
+ self._gb_off_ms = 1000\r
+\r
+ EventMesh.subscribe("set_led_state", self.set_led_state)\r
+ EventMesh.subscribe("get_led_debug_state", self.get_led_debug_state)\r
+ # LED 电平极性:有些板子是低电平点亮(active-low),也可能仅某一路反相\r
+ self._inv_r = 0\r
+ self._inv_g = 0\r
+ self._inv_b = 0\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ inv = vc_config.get_attr("LED_ACTIVE_LOW_RGB", None)\r
+ if isinstance(inv, (tuple, list)) and len(inv) == 3:\r
+ self._inv_r = 1 if inv[0] else 0\r
+ self._inv_g = 1 if inv[1] else 0\r
+ self._inv_b = 1 if inv[2] else 0\r
+ else:\r
+ all_inv = 1 if vc_config.get_bool("LED_ACTIVE_LOW", False) else 0\r
+ self._inv_r = all_inv\r
+ self._inv_g = all_inv\r
+ self._inv_b = all_inv\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._apply_by_table()\r
+ except Exception:\r
+ try:\r
+ self._set_off()\r
+ except Exception:\r
+ pass\r
+\r
+ def _timer_stop(self):\r
+ try:\r
+ self._timer.stop()\r
+ except Exception:\r
+ pass\r
+\r
+ def _set_rgb(self, r, g, b):\r
+ try:\r
+ rr = 1 if r else 0\r
+ gg = 1 if g else 0\r
+ bb = 1 if b else 0\r
+ if self._inv_r:\r
+ rr = 0 if rr else 1\r
+ if self._inv_g:\r
+ gg = 0 if gg else 1\r
+ if self._inv_b:\r
+ bb = 0 if bb else 1\r
+ self.led_red.write(rr)\r
+ self.led_green.write(gg)\r
+ self.led_blue.write(bb)\r
+ except Exception:\r
+ pass\r
+\r
+ def _set_off(self):\r
+ self._mode = "off"\r
+ self._timer_stop()\r
+ self._set_rgb(0, 0, 0)\r
+\r
+ def _set_red_on(self):\r
+ self._mode = "red_on"\r
+ self._timer_stop()\r
+ self._set_rgb(1, 0, 0)\r
+\r
+ def _set_green_on(self):\r
+ self._mode = "green_on"\r
+ self._timer_stop()\r
+ self._set_rgb(0, 1, 0)\r
+\r
+ def _set_green_blink(self, on_ms=1000, off_ms=None):\r
+ """待机/空闲:绿灯慢闪(替代呼吸灯,数字 GPIO 整颗亮灭)。off_ms 缺省则与 on_ms 对称。"""\r
+ on_ms = int(on_ms or 1000)\r
+ if off_ms is None:\r
+ off_ms = on_ms\r
+ else:\r
+ off_ms = int(off_ms or on_ms)\r
+ try:\r
+ if (self._mode == "green_blink" and int(self._gb_on_ms) == int(on_ms)\r
+ and int(self._gb_off_ms) == int(off_ms)):\r
+ return\r
+ except Exception:\r
+ pass\r
+ self._mode = "green_blink"\r
+ self._gb_on_ms = on_ms\r
+ self._gb_off_ms = off_ms\r
+ self._period_ms = on_ms\r
+ self._blink_on = 1\r
+ self._timer_stop()\r
+ self._set_rgb(0, 1, 0)\r
+ self._timer.start(self._gb_on_ms, 0, self._timer_cb)\r
+\r
+ def _set_blue_blink(self, period_ms=1000):\r
+ self._mode = "blue_blink"\r
+ self._period_ms = period_ms or 1000\r
+ self._blink_on = 1\r
+ self._timer_stop()\r
+ self._set_rgb(0, 0, 1)\r
+ self._timer.start(self._period_ms, 1, self._timer_cb)\r
+\r
+ def _set_red_blink(self, period_ms=1000):\r
+ self._mode = "red_blink"\r
+ self._period_ms = period_ms or 1000\r
+ self._blink_on = 1\r
+ self._timer_stop()\r
+ self._set_rgb(1, 0, 0)\r
+ self._timer.start(self._period_ms, 1, self._timer_cb)\r
+\r
+ def _set_bg_alt(self, period_ms=1000):\r
+ self._mode = "bg_alt"\r
+ self._period_ms = period_ms or 1000\r
+ self._bg_phase = 0\r
+ self._timer_stop()\r
+ self._set_rgb(0, 0, 1)\r
+ self._timer.start(self._period_ms, 1, self._timer_cb)\r
+\r
+ def _set_alert_rb_flash(self, cycles=2, period_ms=250):\r
+ """REQ-ERR-01/03:红蓝快闪(约 2 次/秒),结束后回到 _apply_by_table。"""\r
+ try:\r
+ cycles = int(cycles or 2)\r
+ except Exception:\r
+ cycles = 2\r
+ if cycles < 1:\r
+ cycles = 1\r
+ try:\r
+ period_ms = int(period_ms or 250)\r
+ except Exception:\r
+ period_ms = 250\r
+ if period_ms < 100:\r
+ period_ms = 100\r
+ self._mode = "alert_rb"\r
+ self._st_alert = "rb_flash_2"\r
+ self._alert_period_ms = period_ms\r
+ self._alert_steps_left = cycles * 2\r
+ self._alert_rb_phase = 0\r
+ self._timer_stop()\r
+ self._set_rgb(1, 0, 0)\r
+ self._timer.start(self._alert_period_ms, 0, self._timer_cb)\r
+\r
+ def _set_red_flash_limited(self, flashes=3, period_ms=500):\r
+ """REQ-BAT-03:红快闪若干次后熄灭(约 2 次/秒)。"""\r
+ try:\r
+ flashes = int(flashes or 3)\r
+ except Exception:\r
+ flashes = 3\r
+ if flashes < 1:\r
+ flashes = 1\r
+ try:\r
+ period_ms = int(period_ms or 500)\r
+ except Exception:\r
+ period_ms = 500\r
+ if period_ms < 100:\r
+ period_ms = 100\r
+ self._mode = "red_flash_n"\r
+ self._red_flash_steps_left = flashes * 2\r
+ self._alert_period_ms = period_ms\r
+ self._blink_on = 1\r
+ self._timer_stop()\r
+ self._set_rgb(1, 0, 0)\r
+ self._timer.start(self._alert_period_ms, 0, self._timer_cb)\r
+\r
+ def _timer_cb(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._timer_cb_})\r
+\r
+ def _timer_cb_(self):\r
+ if self._mode == "green_blink":\r
+ self._blink_on = 0 if self._blink_on else 1\r
+ self._set_rgb(0, 1 if self._blink_on else 0, 0)\r
+ next_ms = self._gb_on_ms if self._blink_on else self._gb_off_ms\r
+ try:\r
+ self._timer.start(next_ms, 0, self._timer_cb)\r
+ except Exception:\r
+ pass\r
+ return\r
+ if self._mode == "blue_blink":\r
+ self._blink_on = 0 if self._blink_on else 1\r
+ self._set_rgb(0, 0, 1 if self._blink_on else 0)\r
+ return\r
+ if self._mode == "red_blink":\r
+ self._blink_on = 0 if self._blink_on else 1\r
+ self._set_rgb(1 if self._blink_on else 0, 0, 0)\r
+ return\r
+ if self._mode == "red_flash_n":\r
+ self._red_flash_steps_left -= 1\r
+ if self._red_flash_steps_left <= 0:\r
+ self._set_off()\r
+ return\r
+ self._blink_on = 0 if self._blink_on else 1\r
+ self._set_rgb(1 if self._blink_on else 0, 0, 0)\r
+ try:\r
+ self._timer.start(self._alert_period_ms, 0, self._timer_cb)\r
+ except Exception:\r
+ pass\r
+ return\r
+ if self._mode == "bg_alt":\r
+ if self._bg_phase == 0:\r
+ self._set_rgb(0, 1, 0)\r
+ self._bg_phase = 1\r
+ else:\r
+ self._set_rgb(0, 0, 1)\r
+ self._bg_phase = 0\r
+ return\r
+ if self._mode == "alert_rb":\r
+ self._alert_steps_left -= 1\r
+ if self._alert_steps_left <= 0:\r
+ self._st_alert = None\r
+ try:\r
+ self._apply_by_table()\r
+ except Exception:\r
+ self._set_off()\r
+ return\r
+ if self._alert_rb_phase == 0:\r
+ self._set_rgb(0, 0, 1)\r
+ self._alert_rb_phase = 1\r
+ else:\r
+ self._set_rgb(1, 0, 0)\r
+ self._alert_rb_phase = 0\r
+ try:\r
+ self._timer.start(self._alert_period_ms, 0, self._timer_cb)\r
+ except Exception:\r
+ pass\r
+ return\r
+\r
+ def set_led_state(self, event=None, msg=None):\r
+ # msg: dict {ai_state, charging, battery, signal, alert}\r
+ try:\r
+ if isinstance(msg, dict):\r
+ if "alert" in msg:\r
+ aval = msg.get("alert")\r
+ if aval in ("rb_flash_2", "rb_flash", "rb_flash2"):\r
+ self._set_alert_rb_flash(2, 250)\r
+ return\r
+ if aval in ("red_flash_3", "red_flash3"):\r
+ try:\r
+ n = 3\r
+ ms = 500\r
+ if "vc_config" in globals() and vc_config:\r
+ n = int(vc_config.get_int("BATTERY_EMPTY_FLASH_COUNT", 3))\r
+ ms = int(vc_config.get_int("BATTERY_EMPTY_FLASH_PERIOD_MS", 500))\r
+ self._set_red_flash_limited(n, ms)\r
+ except Exception:\r
+ self._set_red_flash_limited(3, 500)\r
+ return\r
+ if aval in (None, "", "none", "clear"):\r
+ self._st_alert = None\r
+ if "ai_state" in msg:\r
+ self._st_ai = msg.get("ai_state") or "idle"\r
+ if "charging" in msg:\r
+ self._st_charging = msg.get("charging") or "none"\r
+ if "battery" in msg:\r
+ self._st_battery = msg.get("battery") or "normal"\r
+ if "signal" in msg:\r
+ self._st_signal = msg.get("signal") or "normal"\r
+ except Exception:\r
+ pass\r
+ if self._mode in ("alert_rb", "red_flash_n"):\r
+ return\r
+ if self._led_state_debug_enabled() and isinstance(msg, dict):\r
+ try:\r
+ print("[LED] set_led_state in={}".format(msg))\r
+ except Exception:\r
+ pass\r
+ self._apply_by_table(msg)\r
+\r
+ def _led_state_debug_enabled(self):\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ return vc_config.get_bool("LED_STATE_DEBUG", True)\r
+ except Exception:\r
+ pass\r
+ return True\r
+\r
+ def _led_log(self, tag, extra=""):\r
+ if not self._led_state_debug_enabled():\r
+ return\r
+ try:\r
+ print("[LED] {} ai={} chg={} bat={} sig={} -> mode={} period_ms={} {}".format(\r
+ tag, self._st_ai, self._st_charging, self._st_battery, self._st_signal,\r
+ self._mode, self._period_ms, extra))\r
+ except Exception:\r
+ pass\r
+\r
+ def _apply_by_table(self, _msg=None):\r
+ if self._mode in ("alert_rb", "red_flash_n"):\r
+ return\r
+ _prev_mode = self._mode\r
+ # P0:低电/空电(高于 AI 交互灯;充电中不显示,走充电灯态)\r
+ try:\r
+ if self._st_charging not in ("charging", "full"):\r
+ if self._st_battery == "empty":\r
+ try:\r
+ ms = 500\r
+ if "vc_config" in globals() and vc_config:\r
+ ms = int(vc_config.get_int("BATTERY_EMPTY_FLASH_PERIOD_MS", 500))\r
+ self._set_red_blink(ms)\r
+ except Exception:\r
+ self._set_red_blink(500)\r
+ return\r
+ if self._st_battery == "low":\r
+ self._set_red_blink(1000)\r
+ return\r
+ except Exception:\r
+ pass\r
+ # P1~P3:AI 阶段(播放 > 思考 > 聆听)\r
+ try:\r
+ if self._st_ai == "playing":\r
+ self._set_green_on()\r
+ return\r
+ if self._st_ai == "thinking":\r
+ # 蓝绿「交替」:约 0.5 s 换一色(`period_ms` 为换色间隔)\r
+ self._set_bg_alt(500)\r
+ return\r
+ if self._st_ai == "listening":\r
+ # 长对话蓝慢闪(≈1s);单对话 PTT 蓝快闪(≈2次/s,REQ-AI-07)\r
+ period_ms = 1000\r
+ try:\r
+ if EventMesh.publish("get_storage_config", "VC_mode") == 1:\r
+ period_ms = 250\r
+ except Exception:\r
+ pass\r
+ self._set_blue_blink(period_ms)\r
+ return\r
+ except Exception:\r
+ pass\r
+ # AI 交互中(聆听/思考/播放/流播)不让充电灯抢占;交互结束 idle 后恢复充电红/充满绿(REQ 冲突规避)\r
+ _skip_charge_led = 0\r
+ try:\r
+ if self._st_ai in ("listening", "thinking", "playing"):\r
+ _skip_charge_led = 1\r
+ except Exception:\r
+ pass\r
+ if not _skip_charge_led:\r
+ try:\r
+ if EventMesh.publish("get_vc_stream_active") == 1:\r
+ _skip_charge_led = 1\r
+ except Exception:\r
+ pass\r
+ # 基础态:充电/电量/信号\r
+ try:\r
+ if not _skip_charge_led:\r
+ if self._st_charging == "full":\r
+ self._set_green_on()\r
+ return\r
+ if self._st_charging == "charging":\r
+ self._set_red_on()\r
+ return\r
+ if self._st_signal == "weak":\r
+ # 弱网:蓝绿「慢交替」,与 thinking 同节拍定义(约 1 s 换一色)\r
+ self._set_bg_alt(1000)\r
+ return\r
+ except Exception:\r
+ pass\r
+ # 空闲待机:默认灭灯(REQ-BAT-01 / GAP-005 方案甲);LED_IDLE_MODE=green_blink 可恢复旧工程绿灯慢闪\r
+ try:\r
+ idle_mode = "off"\r
+ if "vc_config" in globals() and vc_config:\r
+ _im = vc_config.get_attr("LED_IDLE_MODE", None)\r
+ if _im is not None:\r
+ idle_mode = str(_im).strip().lower()\r
+ if idle_mode in ("green_blink", "blink", "green"):\r
+ idle_on = 1000\r
+ if "vc_config" in globals() and vc_config:\r
+ idle_on = int(vc_config.get_int("LED_IDLE_GREEN_BLINK_MS", 1000))\r
+ idle_off = idle_on\r
+ if "vc_config" in globals() and vc_config:\r
+ _ioff = vc_config.get_attr("LED_IDLE_GREEN_BLINK_OFF_MS", None)\r
+ if _ioff is not None:\r
+ try:\r
+ idle_off = int(_ioff)\r
+ except Exception:\r
+ idle_off = idle_on\r
+ if idle_on < 400:\r
+ idle_on = 400\r
+ if idle_off < 400:\r
+ idle_off = 400\r
+ self._set_green_blink(idle_on, idle_off)\r
+ else:\r
+ self._set_off()\r
+ except Exception:\r
+ self._set_off()\r
+ if self._mode != _prev_mode:\r
+ self._led_log("apply_table", "in={}".format(_msg))\r
+\r
+ def get_led_debug_state(self, event=None, msg=None):\r
+ # 供按键/状态机打印当前 LED 聚合状态与最终模式\r
+ try:\r
+ return {\r
+ "ai": self._st_ai,\r
+ "charging": self._st_charging,\r
+ "battery": self._st_battery,\r
+ "signal": self._st_signal,\r
+ "mode": self._mode,\r
+ "period_ms": self._period_ms,\r
+ }\r
+ except Exception:\r
+ return {"mode": getattr(self, "_mode", "?")}\r
+\r
+\r
+def _default_battery_ocv_map():\r
+ # 与 mgr.py BATTERY_OCV_TABLE nix_coy_mnzo2 默认一致;可被 default_config.BATTERY_OCV_MAP 覆盖\r
+ return {\r
+ 4290: 100, 4237: 95, 4179: 90, 4123: 85, 4070: 80, 4020: 75, 3963: 70, 3919: 65, 3882: 60, 3847: 55,\r
+ 3818: 50, 3794: 45, 3772: 40, 3752: 35, 3733: 30, 3716: 25, 3699: 20, 3677: 15, 3652: 10, 3629: 5, 3400: 0,\r
+ }\r
+\r
+\r
+def _parse_battery_ocv_map(raw):\r
+ """解析 default_config.BATTERY_OCV_MAP:dict 或 [(mv, soc), ...]。"""\r
+ if raw is None:\r
+ return None\r
+ out = {}\r
+ try:\r
+ if isinstance(raw, dict):\r
+ pairs = raw.items()\r
+ elif isinstance(raw, (tuple, list)):\r
+ pairs = ((p[0], p[1]) for p in raw if isinstance(p, (tuple, list)) and len(p) >= 2)\r
+ else:\r
+ return None\r
+ for k, v in pairs:\r
+ out[int(k)] = int(v)\r
+ return out if out else None\r
+ except Exception:\r
+ return None\r
+\r
+\r
+def _soc_from_vbatt_mv(volt_mv, ocv_map, ocv_volts, vbatt_simple=None):\r
+ """由电压(mV) 算 SoC(0~100);配置来自 default_config / _BatteryCfg。"""\r
+ try:\r
+ v = int(volt_mv)\r
+ except Exception:\r
+ return None\r
+ try:\r
+ if vbatt_simple:\r
+ _full, _v20, _v5 = vbatt_simple\r
+ if v >= int(_full):\r
+ return 100\r
+ if v <= int(_v5):\r
+ return 0\r
+ if v <= int(_v20):\r
+ return 10\r
+ return 60\r
+ except Exception:\r
+ pass\r
+ try:\r
+ volts = ocv_volts\r
+ if not volts:\r
+ return None\r
+ pre_volt = 0\r
+ volt_not_under = 0\r
+ soc1 = 0\r
+ soc2 = 0\r
+ for vv in volts:\r
+ if v > vv:\r
+ volt_not_under = 1\r
+ soc1 = ocv_map.get(vv, 0)\r
+ soc2 = ocv_map.get(pre_volt, 0)\r
+ break\r
+ else:\r
+ pre_volt = vv\r
+ if pre_volt == 0:\r
+ return soc1\r
+ if volt_not_under == 0:\r
+ return 0\r
+ return int(soc2 - (soc2 - soc1) * (pre_volt - v) // (pre_volt - vv))\r
+ except Exception:\r
+ return None\r
+\r
+\r
+class _BatteryCfg(object):\r
+ """从 vc_config(default_config) 加载电池参数,供 AI_hw_status / get_battery / AI_dev 共用。"""\r
+ _inst = None\r
+\r
+ def __init__(self):\r
+ self.ocv_map = _default_battery_ocv_map()\r
+ self.ocv_volts = sorted(self.ocv_map.keys(), reverse=True)\r
+ self.charge_full_mv = 4290\r
+ self.low_mv = 3699\r
+ self.empty_mv = 3629\r
+ self.low_soc = 20\r
+ self.empty_soc = 5\r
+ self.vbatt_simple = None\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ self.charge_full_mv = int(vc_config.get_int("VBATT_CHARGE_FULL_MV", 4290))\r
+ self.low_mv = int(vc_config.get_int("VBATT_LOW_MV", 3699))\r
+ self.empty_mv = int(vc_config.get_int("VBATT_EMPTY_MV", 3629))\r
+ self.low_soc = int(vc_config.get_int("BATTERY_LOW_SOC_PERCENT", 30))\r
+ self.empty_soc = int(vc_config.get_int("BATTERY_EMPTY_SOC_PERCENT", 10))\r
+ _ocv_cfg = _parse_battery_ocv_map(vc_config.get_attr("BATTERY_OCV_MAP", None))\r
+ if _ocv_cfg:\r
+ self.ocv_map = _ocv_cfg\r
+ self.ocv_volts = sorted(self.ocv_map.keys(), reverse=True)\r
+ if vc_config.get_bool("USE_SIMPLE_VBATT_THRESH", False):\r
+ self.vbatt_simple = (self.charge_full_mv, self.low_mv, self.empty_mv)\r
+ except Exception:\r
+ pass\r
+\r
+ @classmethod\r
+ def instance(cls):\r
+ if cls._inst is None:\r
+ cls._inst = cls()\r
+ return cls._inst\r
+\r
+ def soc_from_vbatt(self, volt_mv):\r
+ return _soc_from_vbatt_mv(volt_mv, self.ocv_map, self.ocv_volts, self.vbatt_simple)\r
+\r
+\r
+class AI_hw_status(Abstract):\r
+ # 精简硬件状态采集:仅维护 LED 状态机所需的 charging/battery/signal\r
+ def __init__(self):\r
+ # 无条件首行:确认是否进入初始化(避免被异常/注册顺序误判)\r
+ try:\r
+ print("[HW] __init__ enter")\r
+ except Exception:\r
+ pass\r
+ _bcfg = _BatteryCfg.instance()\r
+ self._bcfg = _bcfg\r
+ self._ocv_map = _bcfg.ocv_map\r
+ self._ocv_volts = _bcfg.ocv_volts\r
+ self._charge_full_mv = _bcfg.charge_full_mv\r
+ self._low_soc = _bcfg.low_soc\r
+ self._empty_soc = _bcfg.empty_soc\r
+ self._vbatt_simple = _bcfg.vbatt_simple\r
+ # cached state (avoid repeat publish)\r
+ self._chg = None\r
+ self._bat = None\r
+ self._sig = None\r
+ self._usb = None\r
+ self._charge_full_latched = 0\r
+ self._charge_full_streak = 0\r
+ self._charge_full_streak_need = 2\r
+ self._timer = osTimer()\r
+ self._period_ms = 10000\r
+ self._weak_csq = 10\r
+ self._dbg = 0\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ self._period_ms = vc_config.get_int("HW_STATUS_PERIOD_MS", 10000) or 10000\r
+ self._weak_csq = vc_config.get_int("SIGNAL_WEAK_CSQ", 10) or 10\r
+ self._dbg = 1 if vc_config.get_bool("HW_STATUS_DEBUG", False) else 0\r
+ except Exception:\r
+ pass\r
+ # 若开启了 LED/按键调试,也一并打开硬件监控打印,便于关联分析\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ if vc_config.get_bool("LED_DEBUG", False) or vc_config.get_bool("KEY_LED_DEBUG", False):\r
+ self._dbg = 1\r
+ except Exception:\r
+ pass\r
+\r
+ # 注意:AI_dev 也会注册 USB callback;USB 模块通常只支持一个 callback,后注册会覆盖先注册。\r
+ # 为避免“刚开始有效,后面失效”,这里不直接 setCallback,改为订阅 AI_dev 转发的 USB 状态事件,\r
+ # 并在 10 秒轮询中再读一次 GET_USB_STATE 作兜底。\r
+ try:\r
+ EventMesh.subscribe("hw_usb_state", self._on_usb_state)\r
+ except Exception:\r
+ pass\r
+\r
+ # 初始化:先立即刷新一次,再开启周期轮询(避免 one-shot/覆盖差异导致只跑一次)\r
+ try:\r
+ self._update()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._timer.start(self._period_ms, 1, self._tick_cb)\r
+ except Exception:\r
+ pass\r
+ # 初始化打印(一次)\r
+ try:\r
+ print("[HW] init ok period_ms={} weak_csq={} usb_init={} algo={}".format(\r
+ self._period_ms,\r
+ self._weak_csq,\r
+ self._usb,\r
+ "simple" if getattr(self, "_vbatt_simple", None) else "ocv"))\r
+ if self._dbg == 1:\r
+ print("[HW] init detail vbatt_simple={}".format(getattr(self, "_vbatt_simple", None)))\r
+ except Exception:\r
+ pass\r
+\r
+ def _on_usb_state(self, event=None, msg=None):\r
+ try:\r
+ state = msg\r
+ prev = self._usb\r
+ self._usb = 1 if state == 1 else 0\r
+ if self._usb == 1 and prev != 1:\r
+ self._charge_full_latched = 0\r
+ self._charge_full_streak = 0\r
+ if self._usb == 0:\r
+ self._charge_full_latched = 0\r
+ self._charge_full_streak = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._dbg == 1:\r
+ print("[HW] usb_evt state={} usb={}".format(state, self._usb))\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._update()\r
+ except Exception:\r
+ pass\r
+\r
+ def _tick_cb(self, *args, **kwargs):\r
+ # 定时器回调直接刷新(不依赖 AI_dev 的事件线程)\r
+ try:\r
+ self._tick()\r
+ except Exception:\r
+ pass\r
+\r
+ def _tick(self, *args, **kwargs):\r
+ self._update()\r
+\r
+ def _update(self):\r
+ # 每次刷新都“同步推送一次”当前硬件基础态到 LED(不依赖变化检测)\r
+ # 目的:保证 10 秒轮询和 USB callback 都能及时刷新充电/电量/信号的基础态显示\r
+ # charging/battery\r
+ try:\r
+ v = Power.getVbatt()\r
+ except Exception:\r
+ v = None\r
+ soc = None\r
+ csq = None\r
+ rssi = None\r
+ usb_status = None\r
+ chg = "none"\r
+ bat = "normal"\r
+ sig = "normal"\r
+ try:\r
+ usb_status = self._usb\r
+ if usb_status is None:\r
+ try:\r
+ usb_status = EventMesh.publish("GET_USB_STATE")\r
+ if usb_status is None:\r
+ usb_status = 0\r
+ except Exception:\r
+ usb_status = 0\r
+ # charging:插入后先红灯;勿用瞬时 vbatt≥满电阈值直接判满(充电器接入电压即偏高)\r
+ if usb_status == 1:\r
+ if getattr(self, "_charge_full_latched", 0) == 1:\r
+ chg = "full"\r
+ else:\r
+ if v is not None and v >= int(self._charge_full_mv):\r
+ self._charge_full_streak = int(getattr(self, "_charge_full_streak", 0)) + 1\r
+ need = int(getattr(self, "_charge_full_streak_need", 2) or 2)\r
+ if self._charge_full_streak >= need:\r
+ self._charge_full_latched = 1\r
+ chg = "full"\r
+ else:\r
+ chg = "charging"\r
+ else:\r
+ self._charge_full_streak = 0\r
+ chg = "charging"\r
+ # battery level for LED table\r
+ if usb_status != 1 and v is not None:\r
+ soc = self._bcfg.soc_from_vbatt(v)\r
+ if soc is not None:\r
+ if soc < int(self._empty_soc):\r
+ bat = "empty"\r
+ elif soc < int(self._low_soc):\r
+ bat = "low"\r
+ self._chg = chg\r
+ self._bat = bat\r
+ except Exception:\r
+ pass\r
+\r
+ # signal\r
+ try:\r
+ csq = net.csqQueryPoll()\r
+ # csqQueryPoll 返回 (rssi, ber) 或 int,按保守取 rssi\r
+ try:\r
+ if isinstance(csq, (tuple, list)) and len(csq) > 0:\r
+ rssi = csq[0]\r
+ else:\r
+ rssi = csq\r
+ except Exception:\r
+ rssi = csq\r
+ sig = "weak" if (rssi is not None and int(rssi) >= 0 and int(rssi) < int(self._weak_csq)) else "normal"\r
+ self._sig = sig\r
+ except Exception:\r
+ pass\r
+\r
+ # 统一推送一次当前基础态(一次调用带齐字段,减少来回切换)\r
+ try:\r
+ EventMesh.publish("set_led_state", {"charging": chg, "battery": bat, "signal": sig})\r
+ except Exception:\r
+ pass\r
+\r
+ # 轮询调试打印(每次 10s tick 一次):硬件采样一行 + LED 聚合状态一行内打完\r
+ try:\r
+ if self._dbg == 1:\r
+ led = None\r
+ try:\r
+ led = EventMesh.publish("get_led_debug_state")\r
+ except Exception:\r
+ pass\r
+ print("[HW] usb={} vbatt_mv={} soc={} chg={} bat={} csq={} rssi={} sig={} period_ms={} led={}".format(\r
+ usb_status, v, soc, self._chg, self._bat, csq, rssi, self._sig, self._period_ms, led))\r
+ except Exception:\r
+ pass\r
+\r
+ def _soc_from_vbatt(self, volt_mv):\r
+ return self._bcfg.soc_from_vbatt(volt_mv)\r
+\r
+\r
+def set_ipv4_datacall():\r
+ # 获取第一路的APN,ipType 等信息\r
+ pdpCtx = dataCall.getPDPContext(1)\r
+ if pdpCtx != -1:\r
+ if pdpCtx[0] != 0:\r
+ # 如果不是用户需要的ipv4,使用如下方式配置,使用如下方式配置\r
+ ret = dataCall.setPDPContext(1, 0,pdpCtx[1],pdpCtx[2],pdpCtx[3],pdpCtx[4])\r
+ if ret == 0:\r
+ print('配置成功。')\r
+ # cfun0/1后按照配置的信息进行拨号\r
+ net.setModemFun(0)\r
+ net.setModemFun(1)\r
+ else:\r
+ print('配置失败。')\r
+ else:\r
+ print('已经配置为IPv4')\r
+ else:\r
+ print('获取PDP Context失败。')\r
+\r
+#HEARDER+TOTALLEN+DATELEN+DATA\r
+HEARDER = "FFFE"\r
+TOTALLEN = "00000000"\r
+DATELEN = "0000"\r
+DATA = {}\r
+class AI_UART():\r
+ #串口命令通道\r
+ def __init__(self):\r
+ self.send_count=0\r
+ self.AI_uart = UART(UART.UART4, 230400, 8, 0, 1, 0)\r
+ self.AI_uart.set_callback(self.uart_call)\r
+ \r
+ EventMesh.subscribe("bt_ota_run",self.bt_ota_run)#\r
+ EventMesh.subscribe("start_ota",self.start_ota)\r
+ EventMesh.subscribe("end_ota",self.end_ota)\r
+ EventMesh.subscribe("send_bt_switch_toble",self.send_bt_switch_toble)\r
+ EventMesh.subscribe("send_ble_switch_toble",self.send_ble_switch_toble)\r
+ EventMesh.subscribe("send_ble_scan",self.send_ble_scan)\r
+ EventMesh.subscribe("send_connect_bt_mac",self.send_connect_bt_mac)\r
+ EventMesh.subscribe("send_disconnect_bt_mac",self.send_disconnect_bt_mac)\r
+ EventMesh.subscribe("send_imei_to_ble",self.send_imei_to_ble)\r
+ EventMesh.subscribe("send_get_connect_bt_state",self.send_get_connect_bt_state)\r
+ EventMesh.subscribe("recv_bt_connect_rest",self.recv_bt_connect_rest)\r
+ EventMesh.subscribe("AI_send_bt_ble_switch_toble",self.AI_send_bt_ble_switch_toble)\r
+ EventMesh.subscribe("get_bt_versions",self.get_bt_versions)\r
+ EventMesh.subscribe("adjust_volume_toBle",self.adjust_volume_toBle)\r
+ EventMesh.subscribe("clear_earphone_record",self.clear_earphone_record)\r
+ self.bt_switch = 0\r
+ self.ble_switch = 0\r
+ self.uart_timer = osTimer()\r
+ self.get_bt_connect_timer = osTimer()\r
+ self.get_bt_connect_flag = 0\r
+ self.uart_timer_count = 0\r
+ self.bt_ota_flag = 0\r
+ self.read_ack_flag = 0\r
+ self.ping_json_str = {}\r
+ self.ping_json_str_tmp =""\r
+ self.read_uart_data = ""\r
+ self.bt_versions=""\r
+ try:\r
+ uart_ok = False\r
+ if "vc_config" in globals() and vc_config:\r
+ uart_ok = vc_config.uart_supported()\r
+ except Exception:\r
+ uart_ok = False\r
+ if uart_ok:\r
+ self.uart_send_queue = Queue(100)\r
+ self.uart_send_queue_thread_id = _thread.start_new_thread(self.uart_send_queue_thread, ())\r
+\r
+ self.uart_recv_queue = Queue(100)\r
+ self.uart_recv_queue_thread_id = _thread.start_new_thread(self.uart_recv_queue_thread, ())\r
+ self.send_get_versions()#获取蓝牙版本\r
+ \r
+ def uart_send_queue_thread(self):\r
+ while True:\r
+ data = self.uart_send_queue.get()\r
+ \r
+ #EventMesh.debug_log("Info_queue:data = {}".format(data))\r
+ if data != None:\r
+ try:\r
+ if len(data) != 0:\r
+ print("send_queu_uart_data2:{} data:{}".format(data,data["data"]))\r
+ # json_str =ujson.dumps(data)\r
+ if data["event"] == "uart_send_data":\r
+ print("send_queu_uart_data sucess data:{} {}".format(data["data"],type(data["data"])))\r
+ self.uart_queue_send_data(data["data"])\r
+ if "resend_flag" in data:\r
+ print("send_reset_cb:{} {}".format(data["resend_flag"],data["ACK_time"]))\r
+ if data["resend_flag"] == 1:\r
+ self.send_count = 5#重试5次\r
+ ack_timer = 0 #单次最大等待时间\r
+ count = 0\r
+ if "ACK_time" in data:\r
+ if data["ACK_time"] == None:\r
+ ack_timer = 100#最大重发5次,没ack或者返回error重发\r
+ else:\r
+ ack_timer = data["ACK_time"]\r
+ if ack_timer > 0:\r
+ while self.send_count:\r
+ \r
+ if self.send_count <=0 or self.read_ack_flag == 1:\r
+ print("4G串口重发机制退出:{} self.read_ack_flag:{} json_str:{}".format(self.send_count,self.read_ack_flag,data["data"]))\r
+ break\r
+ if count >= ack_timer:\r
+ count = 0\r
+ print("串口异常,重发:")\r
+ self.uart_queue_send_data(data["data"])\r
+ self.send_count-=1\r
+ count += 1\r
+ utime.sleep_ms(1)\r
+\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ utime.sleep_ms(10)\r
+ \r
+ def uart_recv_queue_thread(self):\r
+ while True:\r
+ data = self.uart_recv_queue.get()\r
+ \r
+ #EventMesh.debug_log("Info_queue:data = {}".format(data))\r
+ if data != None:\r
+ try:\r
+ if len(data) != 0:\r
+ print("recv_queu_uart_data2:{}".format(data))\r
+ # json_str =ujson.dumps(data)\r
+ # queue_data={"event":"uart_recv_data"}\r
+ if data["event"] == "uart_recv_data":\r
+ print("recv_queu_uart_data sucess data:{}".format(type(data["event"])))\r
+ self.pares_uart_data()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+\r
+ def send_imei_to_ble(self,event,msg):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.send_imei_to_ble_cb})\r
+\r
+ def send_imei_to_ble_cb(self):\r
+ self.read_ack_flag=0\r
+ imei = modem.getDevImei()\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "imei",\r
+ "data":str(imei)\r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send imei:{}".format(json_str))\r
+ self.send_data(json_str)\r
+ pass\r
+ def send_connect_bt_mac(self,event,msg):\r
+ self.read_ack_flag=0\r
+ json_data = \\r
+ {\r
+ "event": "connect_bt",\r
+ "data":msg\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ self.send_data(json_str,1,2000)\r
+ # self.send_reset_cb(json_str,0,2000) #每2s发送一次 10ms为单位\r
+ pass\r
+ def send_disconnect_bt_mac(self,event,msg):\r
+ self.read_ack_flag=0\r
+ json_data = \\r
+ {\r
+ "event": "disconnect_bt",\r
+ "data":msg\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ self.send_data(json_str,1)\r
+ # self.send_reset_cb(json_str)\r
+\r
+ pass\r
+ def uart_timer_cb(self, *args, **kwargs):\r
+ self.uart_timer_count+=1\r
+ if self.uart_timer_count >40:\r
+ self.read_uart_data = ""\r
+ if self.uart_timer_count >200:\r
+ self.read_uart_data = ""\r
+ self.uart_timer.stop()\r
+ def split_combined_json(self,combined_str):\r
+ # 将连续的JSON字符串分割为多个独立的JSON字符串列表\r
+ #\r
+ # 参数:\r
+ # combined_str: 多个JSON拼接的字符串\r
+ # 返回:\r
+ # 分割后的\r
+ json_list = []\r
+ start_index = 0\r
+ brace_count = 0 # 用于匹配{}括号\r
+ print("combined_str:{}".format(combined_str))\r
+ for i, char in enumerate(combined_str):\r
+ if char == '{':\r
+ brace_count += 1\r
+ elif char == '}':\r
+ brace_count -= 1\r
+ \r
+ # 当括号完全匹配(count=0)时,说明一个完整的JSON对象结束\r
+ if brace_count == 0:\r
+ # 提取从start_index到当前位置的子串(包含当前})\r
+ json_str = combined_str[start_index:i+1].strip()\r
+ if json_str: # 避免空字符串\r
+ json_list.append(json_str)\r
+ start_index = i + 1 # 下一个JSON的起始位置\r
+ print("json_list{}".format(json_list))\r
+ \r
+ return json_list\r
+ \r
+ def uart_call(self,para):\r
+ queue_data={"event":"uart_recv_data"}\r
+\r
+ self.uart_recv_queue.put(queue_data)\r
+ # print("uart_call start")\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.pares_uart_data})\r
+ # print("uart_call end")\r
+ #print("ai_uart:{}".format(para)) \r
+ #[{"event":""},{},{}]\r
+ def pares_uart_data(self): \r
+ uart_data = self.AI_uart.read()\r
+ self.uart_timer.start(10, 1, self.uart_timer_cb)\r
+ self.uart_timer_count = 0\r
+ ack_flag = 0#是否要返回ack\r
+ print("uart_data:{}",uart_data)\r
+ uart_data_str = uart_data.decode('utf-8')\r
+ \r
+ self.read_uart_data = self.read_uart_data +uart_data_str\r
+ try:\r
+ ujson.loads(self.read_uart_data)\r
+ except Exception as e:\r
+ print("json数据不完整:self.read_uart_data{}".format(self.read_uart_data))\r
+ usys.print_exception(e)\r
+ return\r
+ self.uart_timer.stop()\r
+ self.uart_timer_count = 0\r
+ self.playload_data(self.read_uart_data)\r
+\r
+ # 分割JSON\r
+ # json_strings = self.split_combined_json(uart_data_str)\r
+ \r
+ # # 逐个解析并处理\r
+ # for i, json_str in enumerate(json_strings, 1):\r
+ # print("第{}个JSON{}".format(i,json_str))\r
+ # try:\r
+ # data = ujson.loads(json_str)\r
+ \r
+ # # 在这里添加你的业务逻辑,比如根据event处理不同事件\r
+ # self.uart_timer.stop()\r
+ # self.uart_timer_count = 0\r
+ # self.playload_data(self.read_uart_data)\r
+ # # if data.get("event") == "bt_connect_single":\r
+ \r
+ # # elif data.get("event") == "bt_scan_rest":\r
+ \r
+ # except Exception as e:\r
+ # print("JSON解析错误:原始字符串:")\r
+ # print("-" * 50)\r
+\r
+ \r
+ \r
+ # print("uart_data_str:{}",uart_data_str)\r
+ # if len(uart_data_str) < (HEARDER+1+1):\r
+ # return\r
+ # head = uart_data_str[0:4]\r
+ # pack_flag = uart_data_str[4:5]\r
+ # recvflag = uart_data_str[5:6]\r
+ \r
+ \r
+ # if head == HEARDER:\r
+ # if pack_flag == "1": #分包数据\r
+ # if recvflag == "1": #未发送完成\r
+ # self.ping_json_str_tmp+=uart_data_str[6:]\r
+ # pass\r
+ # else:\r
+ # self.playload_data(self.ping_json_str_tmp)\r
+ # pass\r
+ # else:\r
+ # self.playload_data(uart_data_str[6:])\r
+ \r
+ # else:\r
+ # print("uart is not FFFE:") \r
+\r
+ def playload_data(self,msg):\r
+ ack_flag=0\r
+ try:\r
+ print("playload_data:{}".format(msg))\r
+ jsload_data = ujson.loads(msg)\r
+ #print("AI_client_VC_recv jsload_data = {}".format(jsload_data))\r
+ \r
+ if jsload_data["event"] == "AI_KEY_PRESS" :#AI按键按下\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press}) \r
+ ack_flag = 1\r
+ elif jsload_data["event"] == "BT_AI_KEY_PRESS" :#蓝牙耳机key ai对话模式不支持长按对话模式 只支持短按按键对话模式 重新定义按键\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.BT_AI_key_press}) \r
+ ack_flag = 1\r
+ elif jsload_data["event"] == "AI_PWR_PRESS" :#电源按键按下\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_release}) \r
+ ack_flag = 1\r
+ elif jsload_data["event"] == "bt_scan_rest":#扫描列表 #蓝牙停止扫描后主动向4g发送扫描列表需要发送ack\r
+ EventMesh.publish("send_bt_scan_list",jsload_data)\r
+ ack_flag=1\r
+ elif jsload_data["event"]=="bt_connect_single": #收到蓝牙主动发送过来的单个连接蓝牙耳机的连接状态 需要发送ack\r
+ EventMesh.publish("save_bt_connect_list",jsload_data)\r
+ ack_flag=1\r
+ \r
+ elif jsload_data["event"]=="need_send_bt_connect_list": #发送连接蓝牙连接记录列表 不需要发送ack\r
+ self.get_bt_connect_timer.stop()\r
+ self.get_bt_connect_flag = 0\r
+ EventMesh.publish("save_bt_connect_list",jsload_data) #先保存连接记录再发送连接耳机列表to IOT\r
+ EventMesh.publish("send_bt_connect_list")\r
+ \r
+ elif jsload_data["event"]=="get_ble_switch":\r
+ ble_switch_flag = EventMesh.publish("get_storage_config","switch_ble_state")#ble开关 不需要发送ack\r
+ if ble_switch_flag == None:\r
+ ble_switch_flag = "1"\r
+ EventMesh.publish("send_ble_switch_toble",str(ble_switch_flag))\r
+ \r
+ pass\r
+ elif jsload_data["event"]=="get_bt_switch":\r
+ bt_switch_flag = EventMesh.publish("get_storage_config","switch_bt_state")#bt开关 不需要发送ack\r
+ if bt_switch_flag == None:\r
+ bt_switch_flag = "1"\r
+ EventMesh.publish("send_bt_switch_toble",bt_switch_flag)\r
+ \r
+ pass\r
+ elif jsload_data["event"]=="get_cat1_imei": #蓝牙主动向4G获取imei 4g不需要发送ack 蓝牙收到imei需要回ack给4改\r
+ EventMesh.publish("send_imei_to_ble")\r
+ ack_flag=1\r
+ pass\r
+ elif jsload_data["event"] == "post_bt_connect_rest": #蓝牙主动蓝牙耳机连接状态 需要发送ack给蓝牙\r
+ EventMesh.publish("recv_bt_connect_rest",jsload_data)\r
+ ack_flag=1\r
+\r
+ elif jsload_data["event"] == "clear_earphone_record_ok":#清除蓝牙耳机连接记录\r
+ tts_play_str = "删除成功"\r
+ EventMesh.publish("play_tts",[tts_play_str,1])\r
+ self.read_ack_flag = 1\r
+ \r
+ elif jsload_data["event"] == "versions":\r
+ self.bt_versions = jsload_data["data"]\r
+ print("当前蓝牙版本是:{}".format(self.bt_versions))\r
+ self.read_ack_flag = 1\r
+\r
+ # elif jsload_data["event"] == "kws" :#\r
+ # EventMesh.publish("kws_even")\r
+ elif jsload_data["event"] == "ack" :#\r
+ self.read_ack_flag = 1\r
+ if ack_flag == 1:\r
+ self.send_ack()\r
+ self.read_uart_data = ""\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.read_uart_data = ""\r
+ def recv_bt_connect_rest(self,event,msg):\r
+ date_tmp = utime.localtime()\r
+ time_tmp = utime.mktime(date_tmp)\r
+ if "data" in msg:\r
+ EventMesh.publish("send_bt_connect_state_to_iot",msg)\r
+ pass\r
+ def get_bt_versions(self,event=None,msg=None):\r
+ return self.bt_versions\r
+ def clear_earphone_record(self,event,msg): #清楚所有耳机记录\r
+\r
+ json_data = \\r
+ {\r
+ "event": "clear_earphone_record",\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("clear_earphone_record{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+ def adjust_volume_toBle(self,event,msg): #\r
+ self.read_ack_flag=0\r
+ volume = int(msg)\r
+ if volume <= 10:\r
+ volume = 10\r
+ json_data = \\r
+ {\r
+ "event": "adjust_volume",\r
+ "data":str(volume)\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("adjust_volume_toBle{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+\r
+ def AI_send_bt_ble_switch_toble(self,event,msg):\r
+ # // {\r
+ # // "event":"switch_bt_state",\r
+ # // "data":{\r
+ # // "switch_bt_state":"0"\r
+ # // }\r
+ # // } #格式\r
+ \r
+ self.read_ack_flag=0\r
+ if msg == 1: #AI对话开启蓝牙\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "switch_ble_state",\r
+ "data":{\r
+ "switch_ble_state":"1"\r
+ } \r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("switch_ble_state{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+\r
+ elif msg == 2: #AI对话开启蓝牙\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "switch_ble_state",\r
+ "data":{\r
+ "switch_ble_state":"0"\r
+ } \r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("switch_ble_state{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+ elif msg == 3: #AI对话开启蓝牙\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "switch_bt_state",\r
+ "data":{\r
+ "switch_bt_state":"1"\r
+ } \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("switch_bt_state{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+\r
+ elif msg == 4: #AI对话开启蓝牙\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "switch_bt_state",\r
+ "data":{\r
+ "switch_bt_state":"0"\r
+ } \r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("switch_bt_state{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+\r
+ def send_bt_switch_toble(self,event,msg):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.send_bt_switch_toble_cb,"data":msg})\r
+ def send_bt_switch_toble_cb(self,data):\r
+ # // {\r
+ # // "event":"switch_bt_state",\r
+ # // "data":{\r
+ # // "switch_bt_state":"0"\r
+ # // }\r
+ # // } #格式\r
+ self.read_ack_flag=0\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "switch_bt_state",\r
+ "data":{\r
+ "switch_bt_state":str(data)\r
+ } \r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send_bt_switch_toble{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+\r
+ def send_ble_switch_toble(self,event,msg):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.send_ble_switch_toble_cb,"data":msg})\r
+ def send_ble_switch_toble_cb(self,data):\r
+ # // {\r
+ # // "event":"switch_ble_state",\r
+ # // "data":{\r
+ # // "switch_ble_state":"0"\r
+ # // }\r
+ # // } #格式\r
+ \r
+ self.read_ack_flag=0\r
+ json_data = \\r
+ {\r
+ "event": "switch_ble_state",\r
+ "data":{\r
+ "switch_ble_state":str(data)\r
+ } \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send_ble_switch_toble{}".format(json_str))\r
+ self.send_data(json_str,1) \r
+\r
+ # 重发机制\r
+ def send_reset_cb(self,json_str,need_resend = None,timeOut=None):\r
+ print("send_reset_cb:{} {} {}".format(json_str,need_resend,timeOut))\r
+ if timeOut == None:\r
+ self.send_count = 100#最大重发5次,没ack或者返回error重发\r
+ else:\r
+ self.send_count = timeOut\r
+\r
+ while self.send_count:\r
+ self.send_count-=1\r
+ utime.sleep_ms(10)\r
+ if self.send_count <=0 or self.read_ack_flag == 1:\r
+ print("4G串口重发机制退出:{} self.read_ack_flag:{} json_str:{}".format(self.send_count,self.read_ack_flag,json_str))\r
+ break\r
+ if self.send_count%(self.send_count//10) == 0:\r
+ print("串口异常,重发:")\r
+ self.send_data(json_str)\r
+\r
+ pass\r
+ def send_get_connect_bt_state(self,event,msg):\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "get_connect_bt_state"\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send_get_connect_bt_state{}".format(json_str))\r
+ self.send_data(json_str)\r
+ self.get_bt_connect_flag = 1\r
+ self.get_bt_connect_timer.start(2000,0,self.get_bt_connect_timer_cb)\r
+ def get_bt_connect_timer_cb(self,*agrc):\r
+ if self.get_bt_connect_flag == 1:\r
+ EventMesh.publish("send_bt_connect_list")\r
+ pass\r
+ self.get_bt_connect_flag = 0\r
+ pass\r
+ def send_ble_scan(self,event,msg):\r
+ self.read_ack_flag=0\r
+ # // {\r
+ # // "event":"send_ble_scan",\r
+ # // "data":{\r
+ # // "send_ble_scan":"0"\r
+ # // }\r
+ # // } #格式\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "bt_scan",\r
+ "data":msg \r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send_ble_scan{}".format(json_str))\r
+ self.send_data(json_str,1)\r
+ # self.send_reset_cb(json_str)\r
+\r
+ print("bt_send_end")\r
+ def send_data(self,str_data,need_resend = None,ACK_time =None):\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.uart_supported()):\r
+ return\r
+ except Exception:\r
+ return\r
+ if self.bt_ota_flag == 1:\r
+ print("ota 中不需要上传")\r
+ return\r
+ print("send_data{} {} {}".format(str_data,need_resend,ACK_time))\r
+ queue_data={"event":"uart_send_data","data":str_data}\r
+ if need_resend != None:\r
+ queue_data={"event":"uart_send_data","data":str_data,"resend_flag":1,"ACK_time":ACK_time}\r
+\r
+ self.uart_send_queue.put(queue_data)\r
+ def uart_queue_send_data(self,str_data):\r
+ if self.bt_ota_flag == 1:\r
+ return\r
+ #EventMesh.publish("wakeup_BT")\r
+ print("bt_seuart_queue_send_data start {}".format(str_data))\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ self.AI_uart.write(str_data)\r
+ print("bt_seuart_queue_send_data end {}".format(str_data))\r
+ def led_r_open(self,event=None,msg=None):\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "led_r_open",\r
+ \r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("led_r_open{}".format(json_str))\r
+ self.send_data(json_str)\r
+ def led_g_open(self,event=None,msg=None):\r
+ json_data = \\r
+ {\r
+\r
+ "event": "led_g_open",\r
+\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("led_g_open{}".format(json_str))\r
+ self.send_data(json_str)\r
+ def led_b_open(self,event=None,msg=None):\r
+ json_data = \\r
+ {\r
+\r
+ "event": "led_b_open",\r
+ \r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("led_b_open{}".format(json_str))\r
+ self.send_data(json_str)\r
+ def led_rgb_open(self,event=None,msg=None):\r
+ json_data = \\r
+ {\r
+\r
+ "event": "led_rgb_open",\r
+\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("led_rgb_open{}".format(json_str))\r
+ self.send_data(json_str)\r
+ def led_close(self,event=None,msg=None):\r
+ json_data = \\r
+ {\r
+\r
+ "event": "led_close",\r
+\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("led_close{}".format(json_str))\r
+ self.send_data(json_str)\r
+ \r
+ def send_ack(self):\r
+ json_data = \\r
+ {\r
+\r
+ "event": "ack",\r
+\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send_ack{}".format(json_str))\r
+ self.send_data(json_str)\r
+ def AI_key_press(self):\r
+ self.check_rest_timer_count = 0\r
+ print("AI_key_press")\r
+ EventMesh.publish("AI_key_press")\r
+ def BT_AI_key_press(self):\r
+ self.check_rest_timer_count = 0\r
+ print("BT_AI_key_press")\r
+ EventMesh.publish("BT_AI_key_press")\r
+\r
+ def pwk_key_release(self):\r
+ self.check_rest_timer_count = 0\r
+ #print("pwk_key_release")\r
+ EventMesh.publish("pwk_key_release")\r
+ def send_get_versions(self,event=None,msg=None):\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ #self.read_ack_flag = 0\r
+ json_data = \\r
+ {\r
+ \r
+ "event": "get_versions",\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ print("send_get_versions {}".format(json_str))\r
+ self.send_data(json_str,1)\r
+ #self.AI_uart.write(json_str)\r
+ count =1000\r
+ while True:\r
+ count -=1\r
+ if count <=0 or self.read_ack_flag == 1:\r
+ break\r
+ utime.sleep_ms(1)\r
+\r
+ pass\r
+ def start_ota(self,event=None,msg=None):#通过串口给蓝牙升级\r
+ #EventMesh.publish("wakeup_BT") \r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ print("start_ota!!!")\r
+ print("start_ota!!!")\r
+ json_data = \\r
+ {\r
+ "event": "ota_start",\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ self.AI_uart.write(json_str)\r
+ print(json_str)\r
+ count =100\r
+ while True:\r
+ count -=1\r
+ if count <=0 or self.read_ack_flag == 1:\r
+ break\r
+ utime.sleep_ms(1)\r
+ print("start_ota count:{}!!!".format(count))\r
+\r
+ def end_ota(self,event=None,msg=None):#通过串口给蓝牙升级\r
+ #EventMesh.publish("wakeup_BT") \r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ print("end_ota!!!")\r
+ print("end_ota!!!")\r
+ json_data = \\r
+ {\r
+ "event": "ota_end",\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ self.AI_uart.write(json_str)\r
+ print(json_str)\r
+ count =100\r
+ while True:\r
+ count -=1\r
+ if count <=0 or self.read_ack_flag == 1:\r
+ break\r
+ utime.sleep_ms(1)\r
+ print("end_ota count:{}!!!".format(count))\r
+\r
+ def bt_ota_run(self,event=None,msg=None):#通过串口给蓝牙升级\r
+ # EventMesh.publish("wakeup_BT") \r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ print("bt_ota_run!!!:{}".format(msg))\r
+ print("bt_ota_run!!!")\r
+ print("bt_ota_run!!!")\r
+ self.bt_ota_flag = 1\r
+ self.read_ack_flag = 0\r
+ \r
+ json_data = \\r
+ {\r
+ "event": "ota_start",\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ self.AI_uart.write(json_str)\r
+ print(json_str)\r
+ count =2000#等20秒,擦除flash\r
+ while True:\r
+ count -=1\r
+ if count <=0 or self.read_ack_flag == 1:\r
+ break\r
+ utime.sleep_ms(10)\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ print("ota_start:count:{}".format(count))\r
+ url = msg[0]\r
+ bt_size = msg[1]\r
+ headers_T = {"Content-Type":"application/octet-stream"}\r
+ response = request.get(url = url,headers=headers_T,timeout = 120,decode = False)\r
+ offset = 0\r
+ checksum = 0\r
+ for i in response.content:\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ \r
+ \r
+ data = base64.b64encode(i)\r
+ send_count = 5#最大重发5次,没ack或者返回error重发\r
+ while 1:\r
+ send_count-=1\r
+ self.read_ack_flag = 0\r
+ checksum = 0\r
+ # 对每个字节进行异或运算\r
+ for byte in i:\r
+ checksum ^= byte # 等价于 checksum = checksum ^ byte\r
+ json_data = \\r
+ {\r
+ "event": "ota_data",\r
+ "update":data,\r
+ "offset":offset,\r
+ "checksum":checksum \r
+ }\r
+ print("offset:{},checksum:{}".format(offset,checksum))\r
+ json_str =ujson.dumps(json_data)\r
+ self.AI_uart.write(json_str)\r
+ #utime.sleep_ms(100)\r
+ count =300\r
+ while True:\r
+ count -=1\r
+ if count <=0 or self.read_ack_flag != 0 :\r
+ break\r
+ utime.sleep_ms(10)\r
+ print("ota_data:{} count:{}".format(len(json_str),count))\r
+ if self.read_ack_flag == 1:\r
+ break\r
+ if send_count <=0 :\r
+ return -1\r
+ offset+=len(i)\r
+ if offset < bt_size:\r
+ return -1\r
+ self.read_ack_flag = 0\r
+ json_data = \\r
+ {\r
+ "event": "ota_end",\r
+ }\r
+ json_str =ujson.dumps(json_data)\r
+ self.AI_uart.write(json_str)\r
+ \r
+ count =100\r
+ while True:\r
+ count -=1\r
+ if count <=0 or self.read_ack_flag == 1:\r
+ break\r
+ utime.sleep_ms(1)\r
+ self.bt_ota_flag = 0\r
+ return 0\r
+audio_buf = ['chaobingfatis', 'chenggong', 'didianguanji', 'IMEIhao', 'jianquancuowu', 'key_prompt', 'liuliangtishi', 'Low_power_warning', 'R_CHARGE_FULL', 'R_CHARGE_IN', 'R_CHARGE_OUT', 'R_READ_TIME_0', 'R_READ_TIME_1', 'R_READ_TIME_2', 'R_READ_TIME_3', 'R_READ_TIME_4', 'R_READ_TIME_5', 'R_READ_TIME_6', 'R_READ_TIME_7', 'R_READ_TIME_8', 'R_READ_TIME_9', 'R_READ_TIME_A', 'R_READ_TIME_B', 'R_READ_TIME_C', 'R_READ_TIME_D', 'R_READ_TIME_E', 'R_READ_TIME_F', 'R_READ_TIME_G', 'R_READ_TIME_H', 'R_READ_TIME_I', 'R_READ_TIME_J', 'R_READ_TIME_K', 'R_READ_TIME_L', 'R_READ_TIME_M', 'R_READ_TIME_N', 'R_READ_TIME_O', 'R_READ_TIME_P', 'R_READ_TIME_Q', 'R_READ_TIME_R', 'R_READ_TIME_S', 'R_READ_TIME_T', 'R_READ_TIME_U', 'R_READ_TIME_V', 'R_READ_TIME_W', 'R_READ_TIME_X', 'R_READ_TIME_Y', 'R_READ_TIME_Z', 'send_tone', 'shouhou', 'xinhaoruo1', 'zhengzaiguanji']\r
+class AI_dev(Abstract):\r
+ # AI\r
+ def __init__(self):\r
+ # if(dev_type != "7PIN"):\r
+ # set_ipv4_datacall()\r
+ self.is_wdt = 0 #是否开启看门狗\r
+ #AT+QDAI=1,0,0,1,0,0,1,1PCM音频输出设置为主模式、短同步、16 kHz采样率和256 kHz时钟频率,采样位深为16bit,使用的时间间隙数量为1,并且使用第一个时间间隙。\r
+ try:\r
+ bt_pm_en = False\r
+ pin_bt_on = None\r
+ pin_mot = None\r
+ qdai_cfg = None\r
+ if "vc_config" in globals() and vc_config:\r
+ bt_pm_en = bool(vc_config.get_attr("BT_POWER_MOTOR_ENABLED", False))\r
+ pin_bt_on = vc_config.get_attr("PIN_BT_ON_GPIO", None)\r
+ pin_mot = vc_config.get_attr("PIN_MOT_GPIO", None)\r
+ qdai_cfg = vc_config.get_attr("AUDIO_QDAI_CFG", None)\r
+ except Exception:\r
+ bt_pm_en = False\r
+ pin_bt_on = None\r
+ pin_mot = None\r
+ qdai_cfg = None\r
+\r
+ if bt_pm_en:\r
+ try:\r
+ if pin_bt_on:\r
+ self.PIN_BT_ON = Pin(getattr(Pin, pin_bt_on), Pin.OUT, Pin.PULL_DISABLE, 0)\r
+ if pin_mot:\r
+ self.PIN_MOT = Pin(getattr(Pin, pin_mot), Pin.OUT, Pin.PULL_DISABLE, 1)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ utime.sleep_ms(200)\r
+ if hasattr(self, "PIN_MOT"):\r
+ self.PIN_MOT.write(0)\r
+ if hasattr(self, "PIN_BT_ON"):\r
+ self.PIN_BT_ON.write(1)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if isinstance(qdai_cfg, (tuple, list)) and len(qdai_cfg) == 8:\r
+ audio.set_qdai_cfg(*qdai_cfg) # 用iis做音频输入输出\r
+ except Exception:\r
+ pass\r
+ self.KeyAI_status = 0\r
+ self.KeyAI_timer_count = 0\r
+ self.KeyAI_long_press = 0\r
+ self.KeySos_long_press = 0\r
+ self.KeyAI_press = 0\r
+ self.last_ai_key_time = 0#让ai按键不要频繁调用\r
+ if self.is_wdt == 1:\r
+ self.wdt = WDT(30) #开启看门狗\r
+ # self.wdt_timer = osTimer()\r
+ # self.wdt_timer.start(5000, 1, self.wdt_timer_cb)\r
+ \r
+ count = 0\r
+ while True:\r
+ count += 1\r
+ if count > 20 :\r
+ break\r
+ data = dataCall.getInfo(1, 0)\r
+ print("data call INFO {}".format(data))\r
+ if data[2][0]:\r
+ self.network_state = 1\r
+ break\r
+ print("等待网络 {}...".format(count))\r
+ utime.sleep_ms(500)\r
+ self.KeyAI_timer = osTimer()\r
+ self.KeySOS_timer = osTimer()\r
+ self.play_imei_timer = osTimer()\r
+ self.play_imei_timer_count=0\r
+ self.KeySOS_timer_count = 0\r
+ self.KeySOS_timer_flag = 0\r
+ self._single_power_ai_merged = False\r
+ self._pwk_shutdown_ticks = 30 # 100ms 节拍,默认 3s\r
+ try:\r
+ ai_gpio = "GPIO22"\r
+ pwk_gpio = "GPIO26"\r
+ bt_wakeup_en = False\r
+ wakeup_cta1_gpio = None\r
+ wakeup_bt_gpio = None\r
+ if "vc_config" in globals() and vc_config:\r
+ raw_ai = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22")\r
+ if raw_ai is None or raw_ai == "":\r
+ ai_gpio = None\r
+ else:\r
+ ai_gpio = raw_ai\r
+ pwk_gpio = vc_config.get_attr("PWK_KEY_GPIO", "GPIO26") or "GPIO26"\r
+ bt_wakeup_en = bool(vc_config.get_attr("BT_WAKEUP_4G_ENABLED", False))\r
+ wakeup_cta1_gpio = vc_config.get_attr("EXTINT_WAKEUP_CTA1_GPIO", None)\r
+ wakeup_bt_gpio = vc_config.get_attr("PIN_WAKEUP_BT_GPIO", None)\r
+ try:\r
+ self._single_power_ai_merged = bool(\r
+ vc_config.get_bool("SINGLE_POWER_KEY_AI_MERGED", False)\r
+ ) or (ai_gpio is None)\r
+ except Exception:\r
+ self._single_power_ai_merged = ai_gpio is None\r
+ try:\r
+ _pwk_ms = int(vc_config.get_int("PWK_LONG_PRESS_SHUTDOWN_MS", 3000))\r
+ except Exception:\r
+ _pwk_ms = 3000\r
+ self._pwk_shutdown_ticks = max(1, (_pwk_ms + 99) // 100)\r
+ except Exception:\r
+ ai_gpio = "GPIO22"\r
+ pwk_gpio = "GPIO26"\r
+ bt_wakeup_en = False\r
+ wakeup_cta1_gpio = None\r
+ wakeup_bt_gpio = None\r
+\r
+ if ai_gpio:\r
+ self.extint_AI = ExtInt(\r
+ getattr(ExtInt, ai_gpio),\r
+ ExtInt.IRQ_RISING_FALLING,\r
+ ExtInt.PULL_PU,\r
+ self.keyAI_extint_cb,\r
+ )\r
+ self.extint_AI.enable()\r
+ else:\r
+ self.extint_AI = None\r
+\r
+ self.pwk_key = ExtInt(getattr(ExtInt, pwk_gpio), ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_PU, self.pwk_extint_cb)\r
+\r
+ if bt_wakeup_en:\r
+ try:\r
+ if wakeup_cta1_gpio:\r
+ self.extint_wakeup_cta1 = ExtInt(getattr(ExtInt, wakeup_cta1_gpio), ExtInt.IRQ_RISING, ExtInt.PULL_DISABLE, self.wakeup_extint_cb)\r
+ if wakeup_bt_gpio:\r
+ self.PIN_wakeup_bt = Pin(getattr(Pin, wakeup_bt_gpio), Pin.OUT, Pin.PULL_DISABLE, 0)\r
+ self.extint_wakeup_cta1_timer = osTimer()\r
+ if hasattr(self, "extint_wakeup_cta1"):\r
+ self.extint_wakeup_cta1.enable()\r
+ except Exception:\r
+ pass\r
+\r
+ self.pwk_key_status = 0\r
+ self.pwk_key_timer_count = 0\r
+ self.pwk_key_long_press = 0\r
+ self.pwk_key_press = 0\r
+ self.pwk_key_timer = osTimer()\r
+ self.pwk_is_double_timer = osTimer()\r
+ self.double_click_threshold = 1000#双击间隔时间\r
+ self.pwk_press_count = 0#按下次数\r
+ self.last_key_time = 0 # 上一次有效触发时间\r
+ self.key_debounce_interval = 20 # 防抖窗口20ms,标准值\r
+ #self.pwk_key = ExtInt(ExtInt.GPIO26, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_DISABLE, self.pwk_extint_cb)\r
+ \r
+ self.pwk_key.enable() \r
+ print("AI_dev:1")\r
+ self.btnInfo_queue = Queue(100)\r
+ self.btnInfo_queue_count = 0\r
+ _thread.stack_size(16*1024)\r
+ self.btnInfo_queue_thread_id = _thread.start_new_thread(self.btnInfo_queue_thread, ())\r
+ _thread.stack_size(8*1024)\r
+ EventMesh.subscribe("publish_AI_btnInfo",self.publish_AI_btnInfo)\r
+ EventMesh.subscribe("get_battery",self.get_battery)\r
+ EventMesh.subscribe("audio_montage_play",self.audio_montage_play)\r
+ # EventMesh.subscribe("paly_iccid_imei",self.paly_iccid_imei)\r
+ EventMesh.subscribe("paly_device_res_error",self.paly_device_res_error)\r
+ EventMesh.subscribe("Power_off",self.pwk_key_Long_Press)\r
+ EventMesh.subscribe("wakeup_BT",self.wakeup_BT)\r
+ EventMesh.subscribe("sleep_BT",self.sleep_BT)\r
+ EventMesh.subscribe("sys_sleep_cb",self.sys_sleep_cb)#系统休眠前回调\r
+ EventMesh.subscribe("sys_up_cb",self.sys_up_cb)#系统唤醒后回调\r
+ \r
+ EventMesh.subscribe("GET_USB_STATE",self.get_usb_state)\r
+ EventMesh.subscribe("SET_SPEECH_RATE",self.set_speech_rate)\r
+ EventMesh.subscribe("rest_TB",self.rest_TB)\r
+ \r
+ print("AI_dev:2")\r
+ self.pk = PowerKey()\r
+ self.pk.powerKeyEventRegister(self.powerKey_callback)\r
+ self.usb = USB()\r
+ self.usb.setCallback(self.usb_event_cb)\r
+ self.usb_status = self.usb.getStatus()\r
+ self.usb_charge_flag = 0\r
+ self._usb_evt_last_ms = 0\r
+ self._usb_evt_last_state = -1\r
+ self.Low_power_warning_timer = 0\r
+ self.AI_dev_timer = osTimer()\r
+ self.AI_dev_timer.start(10000, 1, self.AI_dev_timer_cb)\r
+ print("AI_dev:AI_dev_timer")\r
+ self.aud = audio.Audio(0)\r
+ self.Low_power_warning_flag = 0\r
+ self._low_warn_last_ms = 0\r
+ self._empty_shutdown_done = 0\r
+ self.VC_TTS_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置\r
+ if self.VC_TTS_volume == None:\r
+ self.VC_TTS_volume = 100\r
+ self.aud.setVolume(self.VC_TTS_volume //10)\r
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
+ #self.aud.play(1,0,'U:/static/xianyikaiji.mp3')#开机音频\r
+ #EventMesh.publish("audio_montage_play",["xianyikaiji.mp3"])\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.ntptime_cb})\r
+ self.check_rest_timer_count = 0\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.uart_supported():\r
+ self.ai_uart = AI_UART()\r
+ except Exception:\r
+ pass\r
+\r
+ self.Keycall1_press_count = 0\r
+ self.ble_switch_flag_time = osTimer()\r
+ self.ble_switch_flag_time.start(20000, 0, self.ble_switch_flag_time_init)\r
+ # AI_hw_status 先于本模块初始化,开机已插 USB 时需补同步一次\r
+ try:\r
+ if int(self.usb_status or 0) == 1:\r
+ EventMesh.publish("hw_usb_state", 1)\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.usb_event, "data": 1})\r
+ except Exception:\r
+ pass\r
+ def ble_switch_flag_time_init(self,*args):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.ble_switch_flag_time_cb})\r
+ def ble_switch_flag_time_cb(self,*args):\r
+ bt_switch_flag = EventMesh.publish("get_storage_config","switch_bt_state")#bt开关\r
+ if bt_switch_flag == None:\r
+ bt_switch_flag = "1"\r
+ EventMesh.publish("send_bt_switch_toble",str(bt_switch_flag))\r
+ # EventMesh.publish("send_imei_to_ble")\r
+ ble_switch_flag = EventMesh.publish("get_storage_config","switch_ble_state")#ble开关\r
+ if ble_switch_flag == None:\r
+ ble_switch_flag = "1"\r
+ EventMesh.publish("send_ble_switch_toble",str(ble_switch_flag))\r
+ vc_tts_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")\r
+ if vc_tts_volume == None:\r
+ vc_tts_volume=100\r
+ # EventMesh.publish("adjust_volume_toBle",str(vc_tts_volume))\r
+\r
+ pass\r
+ print("ble_switch_flag_time_cb!!!!!!!")\r
+ def ntptime_cb(self):\r
+ #ntptime.settime(8)\r
+ print("timer:{}!!!!!!!!\r\n".format(utime.localtime()))\r
+ def usb_event_cb(self, state):\r
+ # USB 插拔抖动过滤(日志中 powerKey 与 usb_evt 0/1 连跳会导致误播 OUT/IN)\r
+ try:\r
+ now = utime.ticks_ms()\r
+ last_ms = getattr(self, "_usb_evt_last_ms", 0)\r
+ last_st = getattr(self, "_usb_evt_last_state", -1)\r
+ if last_ms and utime.ticks_diff(now, last_ms) < 800 and last_st != state:\r
+ return\r
+ self._usb_evt_last_ms = now\r
+ self._usb_evt_last_state = state\r
+ except Exception:\r
+ pass\r
+ # 转发给精简硬件监控(AI_hw_status),避免 USB callback 被覆盖导致“后面失效”\r
+ try:\r
+ EventMesh.publish("hw_usb_state", state)\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.usb_event,"data":state})\r
+\r
+ def usb_event(self, state): \r
+ status = state\r
+ self.usb_charge_flag = 0\r
+ if status == 1:#插入USB\r
+ print("charge R_CHARGE_in")\r
+ self.usb_status = 1\r
+ VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
+ if VC_satrt_flag == None:\r
+ VC_satrt_flag = 0\r
+ \r
+ voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
+ if voice_record_satrt_flag == None:\r
+ voice_record_satrt_flag = 0\r
+ if VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
+ self.aud.stop()\r
+ #EventMesh.publish("flicker_led_red")#充电闪灯\r
+ #self.aud.play(1,1,'U:/static/R_CHARGE_IN.mp3')\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
+ EventMesh.publish("set_led_state", {"charging": "charging"})\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("audio_montage_play",["R_CHARGE_IN.mp3"])\r
+ self.Low_power_warning_flag = 0\r
+ try:\r
+ self._empty_shutdown_done = 0\r
+ except Exception:\r
+ pass\r
+ pass\r
+ elif status == 0:#拨出usbs\r
+ self.usb_status = 0\r
+ VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
+ if VC_satrt_flag == None:\r
+ VC_satrt_flag = 0\r
+ voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
+ if voice_record_satrt_flag == None:\r
+ voice_record_satrt_flag = 0\r
+ if VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
+ self.aud.stop()\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
+ EventMesh.publish("set_led_state", {"charging": "none"})\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("audio_montage_play",["R_CHARGE_OUT.mp3"])\r
+ #self.aud.play(1,1,'U:/static/R_CHARGE_OUT.mp3')\r
+ def get_usb_state(self,event,msg):\r
+ return self.usb_status\r
+ \r
+ def set_speech_rate(self,event,msg):\r
+ #msg的取值是0.5~2.0\r
+ #而语速初始值是0,范围是-50~50\r
+ rate = 0\r
+ if msg == 0.5:\r
+ rate = -50\r
+ elif msg < 1:\r
+ rate = (msg - 1)* 100\r
+ elif msg >= 1:\r
+ rate = (msg - 1)* 50\r
+ rate = round(rate)\r
+ print("速率已经设置为:{}".format(rate))\r
+ EventMesh.publish("rate_change",rate)\r
+ return rate\r
+ def btnInfo_queue_thread(self):\r
+ while True:\r
+ data = self.btnInfo_queue.get()\r
+ self.btnInfo_queue_count-=1\r
+ #EventMesh.debug_log("Info_queue:data = {}".format(data))\r
+ if data != None:\r
+ try:\r
+ if "data" in data :\r
+ data["func"](data["data"])\r
+ else :\r
+ data["func"]()\r
+ except Exception as e:\r
+ #EventMesh.debug_log("btnInfo_queue:ERROR:{}".format(e))\r
+ if "data" in data :\r
+ print(data["data"])\r
+ usys.print_exception(e)\r
+ def publish_AI_btnInfo(self,event,msg):\r
+ #print("publish_AI_btnInfo 1")\r
+ self.btnInfo_queue.put(msg)\r
+ self.btnInfo_queue_count+=1\r
+ if self.btnInfo_queue_count > 80:\r
+ self.btnInfo_queue_count =0\r
+ print("事件处理线程队列出问题,重启!!!!!!!!!!!!!!")\r
+ print("线程:{} 队列:{}".format(_thread.threadIsRunning(self.btnInfo_queue_thread_id),self.btnInfo_queue.size()))\r
+ # if _thread.threadIsRunning(self.btnInfo_queue_thread_id):\r
+ # _thread.stop_thread(self.btnInfo_queue_thread_id)\r
+ # _thread.stack_size(10*1024)\r
+ # self.btnInfo_queue_thread_id = _thread.start_new_thread(self.btnInfo_queue_thread, ())\r
+ # _thread.stack_size(8*1024)\r
+ # self.btnInfo_queue.clear()\r
+ # self.btnInfo_queue = Queue(100)\r
+ EventMesh.publish("VC_error") #播放报错\r
+ utime.sleep_ms(1000)\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ Power.powerRestart()\r
+ EventMesh.publish("set_storage_config",["powerRestart",1])\r
+ #print("publish_AI_btnInfo 2")\r
+ \r
+ def sys_sleep_cb(self,event=None,msg=None):#休眠前回调\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
+ return\r
+ except Exception:\r
+ return\r
+ print("sys_sleep_cb!!!!!!!!!!!!!!!")\r
+ EventMesh.publish("sleep_BT")#休眠蓝牙\r
+ self.PIN_wakeup_bt.write(0)\r
+ def sys_up_cb(self,event=None,msg=None):#唤醒前回调\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
+ return\r
+ except Exception:\r
+ return\r
+ print("sys_up_cb!!!!!!!!!!!!!!!")\r
+ self.wakeup_BT()\r
+ self.PIN_wakeup_bt.write(1)\r
+ def rest_TB(self,event=None,msg=None):\r
+ print("rest_TB!!!!!!!!!!!!!!!")\r
+ self.PIN_BT_ON.write(0)\r
+ utime.sleep_ms(100)\r
+ self.PIN_BT_ON.write(1)\r
+ def wakeup_BT(self,event=None,msg=None):#4G唤醒蓝牙\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
+ return\r
+ except Exception:\r
+ return\r
+ print("wakeup_BT!!!!!!!!!!!!!!!")\r
+ wakeup_cta1_io = self.extint_wakeup_cta1.read_level()\r
+ if wakeup_cta1_io == 1:\r
+ return 1\r
+ self.PIN_wakeup_bt.write(0)\r
+ count = 5\r
+ while(True):\r
+ count-=1\r
+ if count<=0:\r
+ return -1\r
+ wakeup_cta1_io = self.extint_wakeup_cta1.read_level()\r
+ print("bt io状态:{}".format(wakeup_cta1_io))\r
+ if wakeup_cta1_io == 1:\r
+ self.PIN_wakeup_bt.write(1)\r
+ return 1\r
+ utime.sleep_ms(100)\r
+ if count%2 == 0:\r
+ self.PIN_wakeup_bt.write(1)\r
+ else:\r
+ self.PIN_wakeup_bt.write(0)\r
+\r
+ \r
+ def sleep_BT(self,event=None,msg=None):#使蓝牙休眠\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
+ return\r
+ except Exception:\r
+ return\r
+ print("sleep_BT!!!!!")\r
+ print("sleep_BT!!!!!")\r
+ print("sleep_BT!!!!!")\r
+ print("sleep_BT!!!!!")\r
+ self.PIN_wakeup_bt.write(0)\r
+ count = 300\r
+ while(True):\r
+ count-=1\r
+ if count<=0:\r
+ return -1\r
+ if self.extint_wakeup_cta1.read_level() == 0:\r
+ return 1\r
+ utime.sleep_ms(1)\r
+ \r
+ \r
+ def wakeup_extint_cb(self,args):#蓝牙唤醒4g\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("BT_WAKEUP_4G_ENABLED", False)):\r
+ return\r
+ except Exception:\r
+ return\r
+ print("wakeup_extint_cb:!!!!!!!!!")\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ self.PIN_wakeup_bt.write(1)\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.extint_wakeup_cta1_cb})\r
+ def extint_wakeup_cta1_cb(self):\r
+ #self.extint_wakeup_cta1_timer.start(100, 1, self.extint_wakeup_cta1_timer_cb)\r
+ pass\r
+ def extint_wakeup_cta1_timer_cb(self, *args, **kwargs):\r
+ if self.extint_wakeup_cta1.read_level() == 0:\r
+ self.extint_wakeup_cta1_timer.stop()\r
+ #self.PIN_wakeup_bt.write(0)\r
+\r
+ def powerKey_callback(self, status):\r
+ print("powerKey_callback:{}".format(status))\r
+ \r
+\r
+ def pwk_extint_cb(self,args):#pwk\r
+ current_time = 0\r
+ interval = 0\r
+\r
+ try:\r
+ #按键电平判断\r
+ press_level = 0\r
+ current_level = self.pwk_key.read_level()\r
+ if current_level != press_level:\r
+ return # 非按下状态,直接返回\r
+\r
+ #消抖处理\r
+ current_time = utime.ticks_ms()\r
+ if utime.ticks_diff(current_time, self.last_key_time) < self.key_debounce_interval:\r
+ return\r
+ self.last_key_time = current_time # 更新最后按键时间\r
+ print("pwk 按键按下")\r
+\r
+ #仅处理"按下瞬间"事件(避免重复触发)\r
+ if len(args) < 2:\r
+ print("args参数不足,跳过")\r
+ return\r
+ self.pwk_key_status = int(args[1])\r
+ if self.pwk_key_status != 1:\r
+ return\r
+\r
+ #双击计数逻辑\r
+ self.pwk_press_count += 1\r
+ current_time = utime.ticks_ms()\r
+\r
+ #第一次按下(启动长按定时器+双击等待)\r
+ if self.pwk_press_count == 1:\r
+ self.pwk_first_press_time = current_time # 记录首次按下时间\r
+ self.pwk_key_long_press = 0 # 重置长按标记\r
+ self.pwk_key_timer_count = 0 # 重置定时器计数\r
+ self.pwk_key_press = 1 # 标记按键按下\r
+ \r
+ # 停止旧定时器,启动长按/单击判定定时器(100ms单次触发,循环靠回调自启)\r
+ self.pwk_key_timer.stop()\r
+ self.pwk_key_timer.start(100, 1, self.pwk_key_timer_cb)\r
+ print("第一次按下,等待第二次按下")\r
+ if getattr(self, "_single_power_ai_merged", False):\r
+ self._merged_pwk_ai_press_start()\r
+\r
+ #第二次按下(双击判断)\r
+ elif self.pwk_press_count == 2:\r
+ # 计算两次按下间隔\r
+ interval = utime.ticks_diff(current_time, self.pwk_first_press_time)\r
+ \r
+ #符合双击阈值:执行双击逻辑\r
+ if interval <= self.double_click_threshold:\r
+ self.pwk_key_timer.stop() # 停止长按定时器(关键:避免触发长按)\r
+ self.pwk_key_double_press() # 执行双击业务\r
+ print("双击生效")\r
+ # 重置所有状态\r
+ self.pwk_press_count = 0\r
+ self.pwk_key_press = 0\r
+ self.pwk_key_timer_count = 0\r
+\r
+ #间隔过长:视为新的第一次按下\r
+ else:\r
+ self.pwk_first_press_time = current_time # 更新首次按下时间\r
+ self.pwk_press_count = 1 # 重置为第一次按下\r
+ print("间隔过长,视为新的第一次按下")\r
+\r
+ #按下次数超过2次(防计数溢出)\r
+ else:\r
+ self.pwk_press_count = 0\r
+ self.pwk_key_timer.stop()\r
+ print("按键次数过多,重置计数")\r
+\r
+ except Exception as e:\r
+ # 异常时重置所有状态,避免卡死\r
+ self.pwk_press_count = 0\r
+ self.pwk_key_press = 0\r
+ self.pwk_key_timer_count = 0\r
+ self.pwk_key_timer.stop()\r
+ usys.print_exception(e)\r
+\r
+\r
+ def pwk_is_double_judge(self, *args):\r
+ # if self.press_count == 1: # 仅一次按下,判定为单击\r
+ \r
+ # 重置计数(无论何种情况,超时后清零)\r
+ self.pwk_press_count = 0\r
+\r
+\r
+ def pwk_key_timer_cb(self, *args, **kwargs):\r
+ EventMesh.publish("set_wait_first_flag",0)\r
+ EventMesh.publish("set_history_board_flag",0)\r
+ EventMesh.publish("set_play_new_voice_state",0)\r
+ EventMesh.publish("set_history_voice_flag",0)\r
+ EventMesh.publish("set_wait_voice_flag",0)\r
+ EventMesh.publish("set_stop_play",0)\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_timer_cb_})\r
+ # def pwk_key_timer_cb_(self) :\r
+ try:\r
+ self.pwk_key_status = self.pwk_key.read_level()\r
+ self.pwk_key_timer_count +=1\r
+ if self.pwk_key_timer_count >= self._pwk_shutdown_ticks:\r
+ #print("pwk_key_long_press {}".format(self.pwk_key_long_press))\r
+ if self.pwk_key_long_press == 0:\r
+ self.pwk_key_long_press = 1\r
+ #EventMesh.publish("AI_key_long_press")\r
+ # EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])\r
+ self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频\r
+ EventMesh.publish("set_led_state", {"battery": "empty"})\r
+ utime.sleep_ms(2000)\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ Power.powerDown()\r
+ #print("AI_key_release")\r
+ #EventMesh.publish("pwk_key_long_press")\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_Long_Press}) \r
+ if self.pwk_key_status == 1:#松开\r
+ #print("pwk_key_timer_cb:{}".format(self.pwk_key_timer_count))\r
+ try:\r
+ key_count = vc_config.get_int("KEY_RELEASE_DEBOUNCE_COUNT", 5)\r
+ except Exception:\r
+ key_count = 5\r
+ if self.pwk_key_timer_count >= key_count: #防抖200ms\r
+ #EventMesh.publish("AI_key_release")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.pwk_key_release})\r
+ self.pwk_key.enable()\r
+ self.pwk_key_timer_count = 0\r
+ self.pwk_key_press = 0\r
+ self.pwk_press_count = 0#按下次数\r
+ self.pwk_key_timer.stop()\r
+ \r
+ except Exception as e: \r
+ usys.print_exception(e) \r
+ \r
+ def _ai_int_disable_for_hold(self):\r
+ if self.extint_AI is not None:\r
+ self.extint_AI.disable()\r
+\r
+ def _ai_int_enable_after_hold(self):\r
+ if self.extint_AI is not None:\r
+ self.extint_AI.enable()\r
+\r
+ def _key_ai_read_level(self):\r
+ if self.extint_AI is not None:\r
+ return self.extint_AI.read_level()\r
+ return self.pwk_key.read_level()\r
+\r
+ def _merged_pwk_ai_press_start(self):\r
+ """单电源键:按下瞬间走原 AI 键按下链路(松手走 AI_key_release,不按电源短按 Close)。"""\r
+ try:\r
+ current_time = utime.ticks_ms()\r
+ if utime.ticks_diff(current_time, self.last_ai_key_time) < 1:\r
+ return\r
+ self.last_ai_key_time = current_time\r
+ if EventMesh.publish("get_wait_voice_flag") == 1:\r
+ print("播放未读")\r
+ EventMesh.publish("set_wait_voicerupt_flag", 1)\r
+ return\r
+ if EventMesh.publish("get_recv_voice") == 1:\r
+ print("收到留言")\r
+ EventMesh.publish("set_voicerupt_flag", 1)\r
+ return\r
+ if self.KeyAI_press == 0:\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
+ if EventMesh.publish("get_play_new_voice_state") == 1:\r
+ EventMesh.publish("set_play_new_voice_state", 0)\r
+ EventMesh.publish("set_VC_stop_play_audio_flag", 1)\r
+ self._ai_int_disable_for_hold()\r
+ self.KeyAI_press = 1\r
+ self.KeyAI_long_press = 0\r
+ self.KeyAI_timer.start(100, 1, self.KeyAI_timer_cb)\r
+ self.KeyAI_timer_count = 0\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+\r
+ def keyAI_extint_cb(self,args):#call 2\r
+ try:\r
+ if self.extint_AI is None:\r
+ return\r
+ #print("AI_btn:keyAI_extint_cb{}".format(args[1]))\r
+ #定义最短触发间隔\r
+ MIN_TRIGGER_INTERVAL = 1 # 可根据需求调整,单位ms\r
+ #记录上次按键触发时间(初始化放类的__init__里:self.last_ai_key_time = 0)\r
+ current_time = utime.ticks_ms() # 获取当前毫秒时间\r
+ #重复按下,直接返回\r
+ if utime.ticks_diff(current_time, self.last_ai_key_time) < MIN_TRIGGER_INTERVAL:\r
+ print("AI按键触发过频,忽略本次操作")\r
+ return\r
+ #更新上次触发时间\r
+ self.last_ai_key_time = current_time\r
+\r
+ self.KeyAI_status = int(args[1])\r
+ if self.KeyAI_status == 0:\r
+ self.KeyAI_status = 1\r
+ else:\r
+ self.KeyAI_status = 0\r
+ \r
+ if self.KeyAI_status == 0:#按下\r
+ if EventMesh.publish("get_wait_voice_flag") == 1:\r
+ print("播放未读")\r
+ EventMesh.publish("set_wait_voicerupt_flag",1)\r
+ return\r
+ if EventMesh.publish("get_recv_voice") == 1:#当处于收到留言的时候\r
+ print("收到留言")\r
+ EventMesh.publish("set_voicerupt_flag",1)\r
+ return\r
+ if self.KeyAI_press == 0 :\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ if EventMesh.publish("get_play_new_voice_state") == 1:\r
+ EventMesh.publish("set_play_new_voice_state",0)\r
+ \r
+ EventMesh.publish("set_VC_stop_play_audio_flag",1)#按键按下不允许播放之前下发的音频数据\r
+ self._ai_int_disable_for_hold()\r
+ self.KeyAI_press = 1\r
+ self.KeyAI_long_press = 0\r
+ self.KeyAI_timer.start(100, 1, self.KeyAI_timer_cb)\r
+ self.KeyAI_timer_count = 0\r
+ #EventMesh.publish("AI_key_press")\r
+ # if dev_type == "7PIN":\r
+ # self.aud.aud_tone_play(10,100)#播放按键音\r
+ \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press}) \r
+ except Exception as e: \r
+ usys.print_exception(e)\r
+ def KeyAI_timer_cb(self, *args, **kwargs):\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.KeyAI_timer_cb_})\r
+ # def KeyAI_timer_cb_(self):\r
+ try:\r
+ self.KeyAI_status = self._key_ai_read_level()\r
+ self.KeyAI_timer_count +=1\r
+ if self.KeyAI_timer_count >= 10*1 : #长按1s \r
+ \r
+ if self.KeyAI_long_press == 0:\r
+ self.KeyAI_long_press = 1\r
+ #EventMesh.publish("AI_key_long_press")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_long_press}) \r
+ try:\r
+ _sos_on = vc_config.get_bool("SOS_ENABLED", True)\r
+ except Exception:\r
+ _sos_on = True\r
+ if _sos_on and self.KeyAI_timer_count >= 10*3 : #长按3s 上一次是短按则拨打sos紧急联系人\r
+ print("long sos phone:{}".format(self.KeySOS_timer_flag))\r
+ \r
+ if self.KeySOS_timer_flag == 1:\r
+ self.KeySOS_timer_flag=0\r
+ self.KeySos_long_press = 1\r
+ self.KeySOS_timer.stop()\r
+ get_sos_flag = EventMesh.publish("get_sos_flag")\r
+ if get_sos_flag == 0:\r
+ EventMesh.publish("set_sos_info_call")\r
+ pass\r
+\r
+ elif self.KeyAI_timer_count >= 10*8:#关机\r
+ try:\r
+ allow_power_off = vc_config.get_bool("AI_LONG_PRESS_POWER_OFF_ENABLED", False)\r
+ except Exception:\r
+ allow_power_off = False\r
+ if allow_power_off:\r
+ EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])\r
+ #self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频\r
+ EventMesh.publish("set_led_state", {"battery": "empty"})\r
+ utime.sleep_ms(2000)\r
+ self.aud.stopPlayStream()\r
+ print("stopAll:8")\r
+ self.aud.stopAll()\r
+ Power.powerDown()\r
+ if self.KeyAI_status == 1:#松开\r
+ #print("KeyAI_timer_cb:{}".format(self.KeyAI_timer_count))\r
+ try:\r
+ key_count = vc_config.get_int("KEY_RELEASE_DEBOUNCE_COUNT", 5)\r
+ except Exception:\r
+ key_count = 5\r
+ if self.KeyAI_timer_count >= key_count: #防抖200ms\r
+ if self.KeySOS_timer_flag == 1:\r
+ self.KeySOS_timer_flag = 0\r
+ self.KeySos_long_press = 0\r
+ self.KeySOS_timer.stop()\r
+ print("KeySOS_timer_flag!!!!!!!!!!!!!!!") #短按两次需要清除标识\r
+\r
+ if self.KeyAI_long_press == 0:\r
+ try:\r
+ _sos_on = vc_config.get_bool("SOS_ENABLED", True)\r
+ except Exception:\r
+ _sos_on = True\r
+ if _sos_on and self.KeySOS_timer_flag == 0:\r
+ self.KeySOS_timer_flag = 1\r
+ self.KeySos_long_press = 0\r
+ self.KeySOS_timer.start(5000, 0, self.KeySos_timer_cb) #短按开启5s定时器 5s之内没有长按3s则清除标识 长按3s拨打sos紧急联系人\r
+ else:\r
+ self.KeySOS_timer_flag = 0\r
+ self.KeySos_long_press = 0\r
+ self.KeySOS_timer.stop()\r
+ \r
+ #EventMesh.publish("AI_key_release")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_release}) \r
+ self._ai_int_enable_after_hold()\r
+ self.KeyAI_timer.stop()\r
+ self.KeyAI_timer_count = 0\r
+ self.KeyAI_press = 0\r
+ self.KeyAI_long_press=0\r
+ \r
+ except Exception as e: \r
+ usys.print_exception(e) \r
+ \r
+ def AI_key_press(self):\r
+ # EventMesh.publish("wakeup_BT")\r
+ \r
+ # if dev_type == "7PIN" or dev_type == "7PIN_coze" :\r
+ # # self.aud.aud_tone_play(10,60)#播放按键音\r
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
+ self.check_rest_timer_count = 0\r
+ print("AI_key_press")\r
+ EventMesh.publish("AI_key_press")\r
+ def KeySos_timer_cb(self,*argc):\r
+ print("KeySos_timer_cb!!!!!!!!!!{} {}".format(self.KeySos_long_press,self.KeySOS_timer_flag))\r
+ \r
+ if self.KeySos_long_press == 1 and self.KeySOS_timer_flag == 1: #清空sos标识\r
+ self.KeySos_long_press=0\r
+ self.KeySOS_timer_flag=0\r
+ \r
+ self.KeySos_long_press=0\r
+ self.KeySOS_timer_flag = 0\r
+ return\r
+ self.KeySOS_timer_flag = 0\r
+ if self.KeySOS_timer_count >= 3:\r
+ self.KeySOS_timer_count = 0\r
+ get_sos_flag = EventMesh.publish("get_sos_flag")\r
+ if get_sos_flag == 0:\r
+ EventMesh.publish("set_sos_info_call")\r
+ self.KeySOS_timer_count = 0\r
+ pass\r
+\r
+ def AI_key_long_press(self):\r
+ self.check_rest_timer_count = 0\r
+ print("AI_key_long_press")\r
+\r
+ EventMesh.publish("AI_key_long_press")\r
+ \r
+ def AI_key_release(self):\r
+ print("AI_key_release")\r
+ # self.KeySOS_timer_count +=1\r
+ # print("self.KeySOS_timer_count:{} {}".format(self.KeySOS_timer_count,self.KeySOS_timer_flag))\r
+ # if self.KeySOS_timer_flag == 0:\r
+ # self.KeySOS_timer_flag = 1\r
+ # self.KeySOS_timer.start(5000, 0, self.KeySos_timer_cb)\r
+ EventMesh.publish("AI_key_release")\r
+ \r
+ def pwk_key_double_press(self):\r
+ self.check_rest_timer_count = 0\r
+ try:\r
+ if getattr(self, "_single_power_ai_merged", False):\r
+ try:\r
+ self.KeyAI_timer.stop()\r
+ except Exception:\r
+ pass\r
+ self.KeyAI_timer_count = 0\r
+ self.KeyAI_press = 0\r
+ self.KeyAI_long_press = 0\r
+ self._ai_int_enable_after_hold()\r
+ except Exception:\r
+ pass\r
+ print("pwk_key_double_press")\r
+ EventMesh.publish("pwk_key_double_press")\r
+\r
+ def pwk_key_Long_Press(self,event=None,msg=None):\r
+ self.check_rest_timer_count = 0\r
+ print("pwk_key_long_press!!!!!!!")\r
+ print("pwk_key_long_press!!!!!!!")\r
+ \r
+ # EventMesh.publish("paly_iccid_imei")\r
+ # return\r
+ EventMesh.publish("audio_montage_play",["zhengzaiguanji.mp3"])\r
+ #self.aud.play(1,0,'U:/static/zhengzaiguanji.mp3')#关机音频\r
+ EventMesh.publish("set_led_state", {"battery": "empty"})\r
+ utime.sleep_ms(3000)\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ Power.powerDown()\r
+ #print("AI_key_release")\r
+ #EventMesh.publish("pwk_key_long_press")\r
+ \r
+ def pwk_key_release(self):\r
+ self.check_rest_timer_count = 0\r
+ #print("pwk_key_release")\r
+ EventMesh.publish("pwk_key_release")\r
+\r
+ \r
+ def wdt_timer_cb(self, *args, **kwargs):\r
+ if self.is_wdt == 1:\r
+ self.wdt.feed()\r
+\r
+ def AI_dev_timer_cb(self, *args, **kwargs):\r
+ #print("AI_dev_timer_cb")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_dev_timer_event}) \r
+ def _battery_empty_shutdown_seq(self):\r
+ """REQ-BAT-03:红快闪 3 次 → 灭灯 → 关机提示音 → 关机(btn 线程执行)。"""\r
+ try:\r
+ EventMesh.publish("battery_priority_mute_vc")\r
+ except Exception:\r
+ pass\r
+ try:\r
+ n, ms = 3, 500\r
+ if "vc_config" in globals() and vc_config:\r
+ n = int(vc_config.get_int("BATTERY_EMPTY_FLASH_COUNT", 3))\r
+ ms = int(vc_config.get_int("BATTERY_EMPTY_FLASH_PERIOD_MS", 500))\r
+ EventMesh.publish("set_led_state", {"alert": "red_flash_3"})\r
+ utime.sleep_ms(n * ms * 2 + 200)\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle", "battery": "normal", "charging": "none"})\r
+ except Exception:\r
+ pass\r
+ try:\r
+ EventMesh.publish("audio_montage_play", ["didianguanji.mp3"])\r
+ utime.sleep_ms(4500)\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ Power.powerDown()\r
+ except Exception:\r
+ pass\r
+\r
+ def AI_dev_timer_event(self):\r
+ #EventMesh.publish("audio_montage_play",["liuliangtishi.mp3"])\r
+ EventMesh.publish("send_power_init_data")\r
+ if self.is_wdt == 1:\r
+ self.wdt.feed()\r
+ VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
+ if VC_satrt_flag == None:\r
+ VC_satrt_flag = 0\r
+ voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
+ if voice_record_satrt_flag == None:\r
+ voice_record_satrt_flag = 0\r
+ Vbatt = Power.getVbatt()\r
+ if self.usb_status == 1 and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
+ EventMesh.publish("set_led_state", {"charging": "charging"})#充电红灯常亮\r
+ except Exception:\r
+ pass\r
+ _bcfg = _BatteryCfg.instance()\r
+ if self.usb_status == 1 and Vbatt >= _bcfg.charge_full_mv and VC_satrt_flag == 0 and voice_record_satrt_flag == 0:\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
+ EventMesh.publish("set_led_state", {"charging": "full"})\r
+ except Exception:\r
+ pass\r
+ if self.usb_charge_flag == 0 :\r
+ EventMesh.publish("audio_montage_play",["R_CHARGE_FULL.mp3"])\r
+ #self.aud.play(1,0,'U:/static/R_CHARGE_FULL.mp3')#充电已满\r
+ self.usb_charge_flag = 1\r
+ try:\r
+ low_soc = int(_bcfg.low_soc)\r
+ empty_soc = int(_bcfg.empty_soc)\r
+ except Exception:\r
+ low_soc, empty_soc = 30, 10\r
+ warn_interval_ms = 300000\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ warn_interval_ms = int(vc_config.get_int("BATTERY_LOW_WARN_INTERVAL_MS", 300000))\r
+ except Exception:\r
+ pass\r
+ soc = None\r
+ try:\r
+ soc = self.get_battery()\r
+ except Exception:\r
+ soc = None\r
+ if self.usb_status == 0 and soc is not None:\r
+ empty_by_soc = soc < empty_soc\r
+ empty_by_mv = Vbatt is not None and Vbatt < int(_bcfg.empty_mv)\r
+ if empty_by_soc or empty_by_mv:\r
+ if getattr(self, "_empty_shutdown_done", 0) == 0:\r
+ self._empty_shutdown_done = 1\r
+ try:\r
+ print("[BAT] empty shutdown soc={} vbatt={}".format(soc, Vbatt))\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._battery_empty_shutdown_seq})\r
+ elif soc < low_soc:\r
+ if voice_record_satrt_flag == 0:\r
+ try:\r
+ now = utime.ticks_ms()\r
+ last = getattr(self, "_low_warn_last_ms", 0)\r
+ if (not last) or utime.ticks_diff(now, last) >= warn_interval_ms:\r
+ self._low_warn_last_ms = now\r
+ try:\r
+ print("[BAT] low warn soc={} interval_ms={}".format(soc, warn_interval_ms))\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("audio_montage_play", ["Low_power_warning.mp3"])\r
+ except Exception:\r
+ pass\r
+ else:\r
+ try:\r
+ self._low_warn_last_ms = 0\r
+ self._empty_shutdown_done = 0\r
+ except Exception:\r
+ pass\r
+ if VC_satrt_flag == 0 and Vbatt is not None and Vbatt >= int(_bcfg.low_mv):\r
+ try:\r
+ if not ("vc_config" in globals() and vc_config and vc_config.get_bool("USE_MIN_HW_STATUS", True)):\r
+ EventMesh.publish("set_led_state", {"charging": "none", "battery": "normal"})\r
+ except Exception:\r
+ pass\r
+\r
+ self.check_rest_timer_count += 1\r
+ if self.check_rest_timer_count >= 8640:#24小时没按键,则关机\r
+ self.pwk_key_Long_Press()\r
+ \r
+ def get_battery(self, event=None, msg=None):\r
+ # 返回 int 型 SoC(0~100):便于 TTS/上报/比较,且 OCV 插值本身比裸电压稳;不做小数避免频繁跳变\r
+ try:\r
+ Vbatt = Power.getVbatt()\r
+ except Exception:\r
+ return 0\r
+ soc = _BatteryCfg.instance().soc_from_vbatt(Vbatt)\r
+ if soc is None:\r
+ return 0\r
+ if soc < 0:\r
+ return 0\r
+ if soc > 100:\r
+ return 100\r
+ return int(soc)\r
+ def _audio_is_battery_priority(self, msg):\r
+ try:\r
+ for i in (msg or []):\r
+ n = str(i)\r
+ if ("Low_power" in n) or ("didianguanji" in n):\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def audio_montage_play(self,event,msg):\r
+ self.wakeup_BT()\r
+ bat_pri = self._audio_is_battery_priority(msg)\r
+ if bat_pri:\r
+ try:\r
+ EventMesh.publish("battery_priority_mute_vc")\r
+ except Exception:\r
+ pass\r
+ else:\r
+ # 仅服务端 TTS PCM 在播时跳过;低电/关机提示优先级更高(见上)\r
+ try:\r
+ if EventMesh.publish("get_vc_tts_playback_active") == 1:\r
+ print("[audio-arb] skip local montage_play during tts_pcm")\r
+ return\r
+ except Exception:\r
+ pass\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ str_data='mp3files='\r
+ for i in msg:\r
+ try:\r
+ str_tmp = i.replace(".mp3","")\r
+ str_data+="{}.mp3+".format(audio_buf.index(str_tmp))\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ str_data = str_data[:-1]\r
+ print("audio_montage_play:{}".format(str_data))\r
+ self.aud.montage_play('U:/static/audio.bin',1,1,str_data)\r
+ self.play_imei_timer_count=1\r
+ self.play_imei_timer.stop()\r
+ self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)\r
+ \r
+ def paly_iccid_imei(self,event,msg):\r
+ \r
+ self.wakeup_BT()\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ str_data = 'mp3files={}.mp3+'.format(audio_buf.index("liuliangtishi"))#流量提示\r
+ imei = modem.getDevImei()\r
+ iccid = sim.getIccid()\r
+ print("paly_iccid_imei:ICCID:{}".format(iccid))\r
+ if iccid != -1 and len(iccid) > 5:\r
+ iccid = iccid.upper()\r
+ if len(iccid) <20:\r
+ for i in range(20-len(iccid)):\r
+ iccid = "0"+iccid\r
+ str_data += '{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+'.format(audio_buf.index("R_READ_TIME_{}".format(iccid[0]))\\r
+ ,audio_buf.index("R_READ_TIME_{}".format(iccid[1])),audio_buf.index("R_READ_TIME_{}".format(iccid[2])),audio_buf.index("R_READ_TIME_{}".format(iccid[3])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(iccid[4])),audio_buf.index("R_READ_TIME_{}".format(iccid[5])),audio_buf.index("R_READ_TIME_{}".format(iccid[6])),audio_buf.index("R_READ_TIME_{}".format(iccid[7])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(iccid[8])),audio_buf.index("R_READ_TIME_{}".format(iccid[9])),audio_buf.index("R_READ_TIME_{}".format(iccid[10])),audio_buf.index("R_READ_TIME_{}".format(iccid[11])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(iccid[12])),audio_buf.index("R_READ_TIME_{}".format(iccid[13])),audio_buf.index("R_READ_TIME_{}".format(iccid[14])),audio_buf.index("R_READ_TIME_{}".format(iccid[15])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(iccid[16])),audio_buf.index("R_READ_TIME_{}".format(iccid[17])),audio_buf.index("R_READ_TIME_{}".format(iccid[18])),audio_buf.index("R_READ_TIME_{}".format(iccid[19])))\r
+ str_data += '{}.mp3+'.format(audio_buf.index("shouhou"))#可联系售后解决,联系方式见售后客服电话是400-886-8777或到微信公众号柒牌SEVENBRAND自行充值:\r
+ self.aud.montage_play('U:/static/audio.bin',1,1,str_data)\r
+ #开定时器不让系统休眠\r
+ self.play_imei_timer_count=1\r
+ self.play_imei_timer.stop()\r
+ self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)\r
+\r
+ def play_imei_timer_cb(self,event=None,msg=None):\r
+ if self.play_imei_timer_count == 1:\r
+ if EventMesh.publish("get_audio_play_state") == 0:\r
+ print("音频文件播放完成")\r
+ self.play_imei_timer_count=0\r
+ self.play_imei_timer.stop()\r
+ return\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+\r
+ #鉴权出错,播放imei号\r
+ def paly_device_res_error(self,event,msg):\r
+ self.wakeup_BT()\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ str_data = 'mp3files={}.mp3+'.format(audio_buf.index("jianquancuowu"))#AI功能异常,请联系客户查询,设备号为:\r
+ imei = modem.getDevImei()\r
+ imei = imei.upper()\r
+ str_data += '{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3+{}.mp3'.format(audio_buf.index("R_READ_TIME_{}".format(imei[0]))\\r
+ ,audio_buf.index("R_READ_TIME_{}".format(imei[1])),audio_buf.index("R_READ_TIME_{}".format(imei[2])),audio_buf.index("R_READ_TIME_{}".format(imei[3])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(imei[4])),audio_buf.index("R_READ_TIME_{}".format(imei[5])),audio_buf.index("R_READ_TIME_{}".format(imei[6])),audio_buf.index("R_READ_TIME_{}".format(imei[7])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(imei[8])),audio_buf.index("R_READ_TIME_{}".format(imei[9])),audio_buf.index("R_READ_TIME_{}".format(imei[10])),audio_buf.index("R_READ_TIME_{}".format(imei[11])),\\r
+ audio_buf.index("R_READ_TIME_{}".format(imei[12])),audio_buf.index("R_READ_TIME_{}".format(imei[13])),audio_buf.index("R_READ_TIME_{}".format(imei[14])))\r
+ #print("str_data:{}".format(str_data))\r
+ self.aud.montage_play('U:/static/audio.bin',1,1,str_data)\r
+ self.play_imei_timer_count=1\r
+ self.play_imei_timer.stop()\r
+ self.play_imei_timer.start(1000,1,self.play_imei_timer_cb)\r
+\r
+#存储信息\r
+class getStorageDataManager(Abstract):#获取内存数据\r
+ def __init__(self):\r
+ self.config_data = {}\r
+ check = ql_fs.path_exists("/usr/static/storage_file/config.json")\r
+ if check == True:\r
+ with open("/usr/static/storage_file/config.json","r") as fd:\r
+ self.config_data = ujson.load(fd)\r
+ else :\r
+ self.config_data = {'old_time': 0, 'flip_sw': 0}\r
+ pass\r
+\r
+ def post_processor_after_instantiation(self):\r
+ EventMesh.subscribe("set_storage_data", self.set_storage_data)\r
+ EventMesh.subscribe("set_storage_config", self.set_storage_config)\r
+ EventMesh.subscribe("get_storage_data", self.get_storage_data)\r
+ EventMesh.subscribe("get_storage_config", self.get_storage_config)\r
+\r
+ def set_storage_data(self, event, msg): \r
+ # EventMesh.debug_log("set_storage_data:{}".format(msg))\r
+ if msg != None:\r
+ self.config_data = msg\r
+ with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:\r
+ ujson.dump(self.config_data,fd)\r
+ \r
+\r
+ def set_storage_config(self,event,msg): \r
+ # EventMesh.debug_log("set_storage_key_data1:{}".format(msg))\r
+ if len(msg) >= 2:\r
+ self.config_data[msg[0]]=msg[1]\r
+ # EventMesh.debug_log("set_storage_key_data2:{}".format(self.config_data[msg[0]]))\r
+ EventMesh.publish("publish_message_event_ui",{"func":self.set_storage_save,"data":0})\r
+ def set_storage_save(self, data): \r
+ with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:\r
+ ujson.dump(self.config_data,fd)\r
+ def get_storage_data(self, event, msg): \r
+ # EventMesh.debug_log("get_storage_data:{}".format(self.config_data))\r
+ return self.config_data \r
+ def get_storage_config(self,event,msg):\r
+ # EventMesh.debug_log("get_storage_key_data1:{}".format(msg))\r
+ b = msg in self.config_data.keys()\r
+ if b == True:\r
+ # EventMesh.debug_log("get_storage_key_data2:{}".format(self.config_data[msg]))\r
+ return self.config_data[msg]\r
+ else :\r
+ return None\r
+ # try:\r
+ # return self.config_data[msg]\r
+ # except:\r
+ # EventMesh.debug_log("get_storage_key_data:none key:{}",msg)\r
+ # return None\r
+#设备资源授权平台\r
+class device_res_author(Abstract):\r
+ \r
+ def __init__(self) :\r
+ print("device_res_author __init__ start !!!!!!!!!!!!!!!!!")\r
+ self.deviceId = modem.getDevImei()#"876226002022574"\r
+ self.SHA1_BLOCK_SIZE = 64\r
+ self.PAD_BLOCK_SIZE = 16\r
+ self.SHA256 = uhashlib.sha256\r
+ self.pad = lambda s: s + chr(self.PAD_BLOCK_SIZE - len(s) % self.PAD_BLOCK_SIZE) * (self.PAD_BLOCK_SIZE - len(s) % self.PAD_BLOCK_SIZE)\r
+ # 除去补16字节的多余字符\r
+ self.unpad = lambda s: s[:-s[-1]]\r
+ # 设备鉴权:优先 default_config / usr/default_config 的 PRODUCT_PID、PRODUCT_KEY\r
+ self.pid = "cqxJZi5CNgzjS3mX21Zw"\r
+ self.key = b"uKLrChwrxA1LklgBc3VwawYjUkbT3BgX"\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ _pid = vc_config.get_attr("PRODUCT_PID", None)\r
+ if _pid:\r
+ self.pid = str(_pid)\r
+ _key = vc_config.get_attr("PRODUCT_KEY", None)\r
+ if _key is not None and _key != "":\r
+ self.key = _key if isinstance(_key, bytes) else str(_key).encode()\r
+ except Exception:\r
+ pass\r
+ self.dev_res_timer = osTimer()\r
+ self.dev_res_timer.start(20000, 1, self.dev_timer_cb)\r
+ self.dev_res_timerCount = 0\r
+ self.ud_token_timer = osTimer()\r
+ self.ud_token_timer.start(10000, 1, self.ud_token_timer_cb)\r
+ self.ud_token_timer_Count = 0\r
+ self.dev_token = None\r
+ self.res_error_count = 0\r
+ self.ntptime_flag = 0\r
+ EventMesh.subscribe("get_dev_token", self.get_dev_token)\r
+ EventMesh.subscribe("update_dev_token", self.update_dev_token)\r
+ EventMesh.subscribe("dev_active", self.dev_active)\r
+ EventMesh.subscribe("dev_sign_in", self.dev_sign_in)\r
+ EventMesh.subscribe("dev_NTP", self.dev_NTP)\r
+ EventMesh.subscribe("post_iccid_user", self.post_iccid_user)\r
+ # self.post_iccidd_token_timer = osTimer()\r
+ # self.post_iccidd_token/_timer.start(10000, 0, self.post_iccidd_token_timer_cb)\r
+ self.connect_web_flag = 0\r
+ #EventMesh.publish_async("dev_NTP",thread_size = 8192)\r
+\r
+ # count = 0\r
+ # while True:\r
+ # if self.ntptime_flag == 1:\r
+ # break\r
+ # utime.sleep_ms(1)\r
+ # count+=1\r
+ # if count>600:\r
+ # break\r
+ # EventMesh.publish_async("dev_active",thread_size = 8192)\r
+ # EventMesh.publish_async("dev_sign_in",thread_size = 8192)\r
+\r
+ self.dev_NTP()#获取时间\r
+ self.dev_active() #设备激活\r
+ self.dev_sign_in() #设备登入\r
+\r
+ \r
+ print("device_res_author __init__ end !!!!!!!!!!!!!!!!!")\r
+ def post_iccidd_token_timer_cb(self,*argc):\r
+ EventMesh.publish("post_iccid_user")\r
+\r
+ def get_dev_token(self, event, msg): \r
+ return self.dev_token\r
+ #网络校时\r
+ def dev_NTP(self, event=None, msg=None):\r
+ response = None\r
+ try:\r
+ print("dev_NTP1")\r
+ url = constants.DMP_BASE + "/dev/time"\r
+ print("dev_NTP:{}".format(url))\r
+ response = request.get(url,timeout=5)\r
+ data = response.json()\r
+ print("dev_NTP:{}".format(data))\r
+ if data:\r
+ try:\r
+ timeSecond = data["data"]["timestamp"]//1000\r
+ \r
+ next_alarm_time = utime.localtime(timeSecond)\r
+ print("timeSecond {} next_alarm_time {}".format(timeSecond,next_alarm_time))\r
+ machine.RTC().datetime((next_alarm_time[0],next_alarm_time[1],next_alarm_time[2],next_alarm_time[6],next_alarm_time[3],next_alarm_time[4],next_alarm_time[5],0))\r
+ self.ntptime_flag = 1\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close() \r
+ print("dev_NTP end")\r
+ def dev_timer_cb(self, *args): #设备资源管理鉴权平台定时器\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.dev_timer_cb_}) \r
+\r
+ def dev_timer_cb_(self): \r
+ #if EventMesh.DEV_RES_MANAGE_AUTHEN_PLATFORM == 1:\r
+ try:\r
+ if self.dev_token == None:\r
+ if self.ntptime_flag == 0:\r
+ self.ntptime_flag = 1\r
+ EventMesh.publish_async("dev_NTP",thread_size = 8192)\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.dev_NTP})\r
+ print("timer:{}!!!!!!!!\r\n".format(utime.localtime()))\r
+ EventMesh.publish_async("dev_active",thread_size = 8192)\r
+ EventMesh.publish_async("dev_sign_in",thread_size = 8192)\r
+ # self.dev_active() #设备激活\r
+ # self.dev_sign_in() #设备登入\r
+ self.dev_res_timerCount+=1\r
+ if self.dev_res_timerCount > 3: #开机一分钟内没有获取到密码和token 等下一次更新令牌在走登入 激活流程\r
+ self.dev_res_timerCount=0\r
+ self.dev_res_timer.stop()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ def ud_token_timer_cb(self, *args): \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.ud_token_timer_cb_}) \r
+\r
+ def ud_token_timer_cb_(self): \r
+ try:\r
+ #print("ud_token_timer_!!!!")\r
+ self.ud_token_timer_Count+=1\r
+ if self.ud_token_timer_Count >= 720: #定时10*3000s时更新token\r
+ self.ud_token_timer_Count = 0\r
+ EventMesh.publish_async("dev_sign_in",thread_size = 8192)\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ def get_sim_Status(self):\r
+ return sim.getStatus() \r
+ def HMAC(self,k, m):\r
+ \r
+ KEY_BLOCK = k + (b'\0' * (self.SHA1_BLOCK_SIZE - len(k)))\r
+ KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)\r
+ KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)\r
+ inner_message = KEY_INNER + m\r
+ outer_message = KEY_OUTER + self.SHA256(inner_message).digest()\r
+ return self.SHA256(outer_message)\r
+ def get_hmac_sha256(self,k, m):\r
+ data = self.HMAC(k, m).digest()\r
+ # print("HMAC(k, m).digest():", data)\r
+ # print("HMAC(k, m).digest():", ubinascii.hexlify(data))\r
+ hex_data = ubinascii.hexlify(data)\r
+ hex_data = hex_data.decode('ascii')\r
+ return hex_data\r
+ # 加密函数\r
+ def aes_CBC_Encrypt(self,data, key, iv): # CBC模式的加密函数,data为明文,key为16字节密钥,iv为16字节的偏移量\r
+ key = key.encode('utf-8')\r
+ iv = iv.encode('utf-8') # CBC 模式下的偏移量\r
+ data = self.pad(data) # 补位\r
+ data = data.encode('utf-8')\r
+ aes = ucryptolib.aes(key,ucryptolib.MODE_CBC,iv)\r
+ result = base64.b64encode(aes.encrypt(data))\r
+ return str(result, 'utf-8') # 以字符串的形式返回\r
+\r
+ # 解密函数\r
+ def aes_CBC_Decrypt(self,data, key, iv): # CBC模式的解密函数,data为密文,key为16字节密钥\r
+ key = key.encode('utf-8')\r
+ iv = iv.encode('utf-8')\r
+ \r
+ aes = ucryptolib.aes(key,ucryptolib.MODE_CBC,iv) #CBC模式\r
+\r
+ # decrypt AES解密 B64decode为base64 转码\r
+ result = aes.decrypt(base64.b64decode(data))\r
+ result = self.unpad(result) # 除去补16字节的多余字符\r
+ return str(result, 'utf-8') # 以字符串的形式返回\r
+ #激活接口 没有密码走激活\r
+ def dev_active(self, event=None, msg=None):\r
+ signal = net.csqQueryPoll()\r
+ if signal == -1 or self.get_sim_Status() != 1:#网络不正常\r
+ print("dev_active 无 SIM卡!!!!!!!!\r\n")\r
+ return\r
+ print("dev_active 1\r\n")\r
+ # 签名内容: = deviceId+pid+timeStamp\r
+ dev_pass = ""\r
+ dev_pass = EventMesh.publish("get_storage_config","dev_pass")\r
+ print("dev_pass:{}\r\n".format(dev_pass))\r
+ if dev_pass == None:\r
+ dev_pass = ""\r
+ if dev_pass == "" or dev_pass == None:\r
+ try:\r
+ date_tmp = utime.localtime() #获取当前时间\r
+ print("date_tmp:{}\r\n".format(date_tmp)) #转换时间戳s\r
+ timeStamp = (utime.mktime(date_tmp))*1000#获取时间戳ms\r
+ deviceId = self.deviceId #"876226002022574" #以imei号\r
+ pid = self.pid\r
+ key = self.key\r
+ sign_data = "{}{}{}".format(deviceId,pid,timeStamp) #签名内容\r
+ print("sign_data:{}\r\n".format(sign_data))\r
+ sha256_hex = self.get_hmac_sha256(key,sign_data)\r
+ print("sha256_hex:{}\r\n".format(sha256_hex))\r
+\r
+ data = {\r
+ "deviceId":"{}".format(deviceId), #设备名\r
+ "pid":pid, #产品PID\r
+ "sign":"{}".format(sha256_hex), #HMACSHA256签名后的数据\r
+ "timeStamp":timeStamp, #要以ms的形式\r
+ "signMethod":"HMACSHA256" #签名方式 HMACSHA256\r
+ }\r
+ print(ujson.dumps(data))\r
+ headers_T ={"Content-Type":"application/json"}\r
+ url = constants.DMP_BASE + "/dev/auth/active"\r
+ print("dev_active:{}".format(url))\r
+ data =ujson.dumps(data)\r
+ response = None\r
+ response_result = 0\r
+ try:\r
+ response = request.put(url,data=data,headers = headers_T,timeout=5)\r
+ response_result = 1\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ print("put error!!!!!\r\n")\r
+ response_result = 0\r
+ \r
+ IV = "" #偏移量\r
+ PASSWORD= "" #服务器返回后的加密内容 密码\r
+ if response_result == 1: #访问成功ip\r
+ if response.status_code == 200:\r
+ print("headers:{}".format(response.headers))\r
+ content = response.content\r
+ c = next(content)\r
+ # "api.xcloudtech.com"\r
+ # d_server_port = 9000\r
+ data = ujson.loads(c)\r
+ # data = response.json()\r
+ print("data-{}".format(data))\r
+ if data != None:\r
+ if "success" in data:\r
+ if data['success'] == True:\r
+ if "data" in data:\r
+ data_tmp = data["data"]\r
+ if "nonce" in data_tmp: #偏移量\r
+ IV = data_tmp["nonce"]\r
+ if "password" in data_tmp:\r
+ PASSWORD = data_tmp["password"]\r
+ # EventMesh.publish("set_storage_config",["dev_pass",PASS])\r
+ if data["code"] == 20001:#鉴权成功\r
+ pass\r
+ elif data["code"] > 50000:#\r
+ self.res_error_count+=1\r
+ if self.res_error_count>=3:\r
+ self.res_error_count = 0\r
+ \r
+ EventMesh.publish("paly_device_res_error")\r
+ if data["code"] == 50003:#时间错误\r
+ self.ntptime_flag = 0\r
+ if PASSWORD != None and IV != None:\r
+ if len(PASSWORD) != 0 and len(IV) != 0:\r
+ password = PASSWORD\r
+ iv = IV\r
+ #解密 之后保存本地下一次登入做使用\r
+ decrypt_data = self.aes_CBC_Decrypt(password, self.key, iv) #对服务器下发的密码进行解密\r
+ print("decrypt_data {}".format(decrypt_data))\r
+ EventMesh.publish("set_storage_config",["dev_pass",decrypt_data]) \r
+ print("IV:{}\r\n PASS:{}".format(IV,PASSWORD))\r
+ if response != None:\r
+ response.close() \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ print("dev_active end\r\n")\r
+ return [response_result,PASSWORD,IV]\r
+ #随机生成16个字节的字符串作为加密的IV 偏移量\r
+ def generate_random_string(self,length=16): \r
+ random_str = ""\r
+ chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\r
+ for i in range(length):\r
+ random_str+=chars[random.randint(0,length)]\r
+ return random_str\r
+\r
+ #设备登入 没有Token 走登入获取token\r
+ def dev_sign_in(self, event=None, msg=None):\r
+ \r
+\r
+ signal = net.csqQueryPoll()\r
+ if signal == -1 or self.get_sim_Status() != 1:#网络不正常\r
+ print("dev_sign_in 无 SIM卡!!!!!!!!\r\n")\r
+ return\r
+ print("dev_sign_in 1")\r
+ token = ""\r
+ try:\r
+ token = self.dev_token\r
+ dev_pass = EventMesh.publish("get_storage_config","dev_pass") \r
+ #print("token:{}\r\n".format(token))\r
+ if token == None:\r
+ token = ""\r
+ if dev_pass == None:\r
+ dev_pass = ""\r
+ if len(dev_pass) == 0:#没有密码\r
+ return\r
+ #if token == None or token == "":\r
+ if 1: #返回结果成功\r
+ #获取本地保存的密码\r
+ decrypt_data=EventMesh.publish("get_storage_config","dev_pass")\r
+ # decrypt_data = self.aes_CBC_Decrypt(password, self.key, iv) #对服务器下发的密码进行解密\r
+ print("decrypt_data {}".format(decrypt_data))\r
+ #随机生成16个字符串 设备生成做加密使用\r
+ random_string = self.generate_random_string(16) #随机的16个字符串\r
+ enc_iv = random_string #加密的IV偏移量\r
+ #设备端进行密码加密\r
+ encrypt_data = self.aes_CBC_Encrypt(decrypt_data, self.key, enc_iv)\r
+ print("encrypt_data {}".format(encrypt_data))\r
+\r
+ #登入数据\r
+ deviceId = self.deviceId\r
+ sign_in_data = {\r
+ "deviceId":"{}".format(deviceId), #设备ID\r
+ "password":"{}".format(encrypt_data), #加密的数据\r
+ "nonce":"{}".format(random_string) #随机的16个字符串\r
+ }\r
+ print(ujson.dumps(sign_in_data))\r
+ sign_url =None\r
+ sign_url = constants.DMP_BASE + "/dev/auth/sign_in"\r
+ #获取验证码\r
+ #url = "http://dmp.qipai.com.cn:30461/dev/device/bind-code/860132066060377"\r
+ # token = EventMesh.publish("get_dev_token")\r
+ # headers_T = {"Content-Type":"application/x-www-form-urlencoded","Accept":"*/*","bz-dt":"{}".format(token)}\r
+ # response = request.get(url,headers = headers_T,timeout=5)\r
+ # data = response.json()\r
+ print("dev_sign_in:{}".format(sign_url))\r
+ headers_T ={"Content-Type":"application/json"}\r
+ response_result = 0\r
+ response = 0\r
+ try:\r
+ response = request.post(sign_url,data=ujson.dumps(sign_in_data),headers = headers_T,timeout=5)\r
+ response_result = 1\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ print("post error!!!!!\r\n")\r
+ response_result = 0\r
+ # {'msg': '', 'code': 20001, 'success': True, \r
+ # 'data': {'deviceId': '876226002022574', \r
+ # 'token': '95436a9e09fd0ff89feb8825257dca982f'}\r
+ # }\r
+ \r
+ #self.dev_token = ""\r
+ if response_result == 1:\r
+ if response.status_code == 200:\r
+ content = response.content\r
+ c = next(content)\r
+ data = ujson.loads(c)\r
+ print("data-{}".format(data))\r
+ if data != None:\r
+ if "success" in data:\r
+ if data['success'] == True: #登入成功\r
+ if "data" in data:\r
+ data_tmp = data["data"]\r
+ if "token" in data_tmp: #token\r
+ self.dev_token = data_tmp["token"]\r
+ else:#报错清空密钥\r
+ print("登录失败,清空密钥")\r
+ EventMesh.publish("set_storage_config",["dev_pass",""])\r
+ if data["code"] == 50003:#时间错误\r
+ self.ntptime_flag = 0\r
+ \r
+ print("token:{}\r\n".format(self.dev_token))\r
+ if response != None:\r
+ response.close()\r
+ pass\r
+ except Exception as e:\r
+ print("post error!!!!!\r\n")\r
+ response_result = 0\r
+ if EventMesh.publish("get_dev_token") != None:\r
+ if len(EventMesh.publish("get_dev_token")) != 0:\r
+ if self.connect_web_flag == 0:\r
+ EventMesh.publish("post_iccid_user")\r
+ self.connect_web_flag = 1\r
+ print("dev_sign_in end")\r
+ #ICCID上传\r
+ def post_iccid_user(self,event=None,msg=None):\r
+ response = None\r
+ try:\r
+ iccid = sim.getIccid()\r
+ if iccid != -1 and len(iccid) > 5:\r
+ print("pots_ICCID:{}".format(iccid))\r
+ #Headers = {'Transfer-Encodong':'chunked'}\r
+ token = EventMesh.publish("get_dev_token")\r
+ headers_T = None\r
+ if token:\r
+ headers_T = {"qp-dt":"{}".format(token)}\r
+ #http://dmp.raisound.com/bz-api/dev/iccid?deviceId=866610001099679&iccid=89860002192307889057\r
+ url =None\r
+ url = (constants.DMP_BASE + "/dev/iccid?deviceId={}&iccid={}").format(modem.getDevImei(),iccid)\r
+ print("{} H:{}".format(url,headers_T))\r
+ response = request.post(url,headers = headers_T)\r
+ if response.status_code == 200:\r
+ data = response.json()\r
+ print("pots_ICCID:{}".format(data))\r
+ else:\r
+ print("pots_ICCID error!!!!")\r
+ for i in response.text:\r
+ print(i)\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+ #获取新的令牌 更新token\r
+ def update_dev_token(self, event=None, msg=None):\r
+ print("update_dev_token")\r
+ try:\r
+ dev_pass = ""\r
+ dev_pass = EventMesh.publish("get_storage_config","dev_pass")\r
+ \r
+ if dev_pass == None:\r
+ dev_pass = ""\r
+ if len(dev_pass) == 0: #没有密码走激活\r
+ print("no dev_pass:{}\r\n".format(dev_pass))\r
+ EventMesh.publish("dev_active")\r
+ token = self.dev_token\r
+ if token == None:\r
+ token = ""\r
+ if len(token) == 0: #没有token走登入\r
+ print("no dev_token:{}\r\n".format(token))\r
+ #self.dev_sign_in()\r
+ EventMesh.publish("dev_sign_in")\r
+ dev_tokenip =None\r
+ dev_tokenip = constants.DMP_BASE + "/dev/auth/token"\r
+ \r
+ token = self.dev_token\r
+ if token == None:\r
+ token = ""\r
+ if len(token) != 0:\r
+ response_result=0\r
+ response = None\r
+ headers_T = {"bz-dt":"{}".format(token)}\r
+ try:\r
+ response = request.get(dev_tokenip,headers = headers_T,timeout=5)\r
+ response_result = 1\r
+ except Exception as e:\r
+ print("get error!!!!!\r\n")\r
+ response_result = 0\r
+ if response_result == 1:\r
+ if response.status_code == 200:\r
+ try:\r
+ headers_data = response.headers\r
+ # print("headers_data!!!!! headers_data:{}\r\n".format(headers_data))\r
+ if "bz-dt" in headers_data: #bz-dt字段是否在头部数据里面 如果有就需要更新\r
+ bz_dt = headers_data["bz-dt"] \r
+ self.dev_token = bz_dt.replace(" ", "") #去掉所有的空格符\r
+ print("update_dev_token:{}".format(self.dev_token))\r
+ except Exception as e:\r
+ print("headers_data!!!!! error\r\n")\r
+ if response != None:\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ print("update_dev_token end")\r
+\r
+\r
+\r
+ \r
+#OTA升级\r
+class OTA():\r
+ def __init__(self):\r
+ #EventMesh.subscribe("min_sys_ota",self.min_sys_ota)\r
+ self.downloadFileVoURL1= ""\r
+ self.downloadFileVoURL2= ""\r
+ self.downloadFileVoURLBT=""\r
+ self.BT_ota_size =0\r
+ self.ota_task_id = None\r
+ #蓝牙升级升级,返回0,没有升级;返回,1有升级,正在执行升级;返回2,电压不够\r
+ def bt_ota(self,event=None,msg=None):\r
+ print("bt_ota_ota")\r
+ signal = net.csqQueryPoll()\r
+ if signal == -1 or sim.getStatus() != 1:#网络不正常\r
+ print("bt_ota 无 SIM卡!!!!!!!!\r\n")\r
+ return 0\r
+ # return 0\r
+ response = None\r
+ ret_str =0\r
+\r
+ self.downloadFileVoURLBT=""\r
+ try:\r
+ #sourceVersion = versions.Build_Time\r
+ # sourceVersion = "7PIN-"+sourceVersion[:10]+"-"+sourceVersion[11:-3]\r
+ # print("sourceVersion:{}".format(sourceVersion))\r
+ # sourceVersion = sourceVersion.replace(" ", "0")\r
+ # print("sourceVersion2:{}".format(sourceVersion))\r
+ sourceVersion = version_util.get_unified_version()\r
+ print("sourceVersion:{}".format(sourceVersion))\r
+ bt_versions = EventMesh.publish("get_bt_versions")\r
+ if bt_versions == None:\r
+ bt_versions=""\r
+ print("bt_versions:{}".format(bt_versions))\r
+ #url = "http://iot.api.thisiot.com/ota/task?deviceId=860132060348836&sourceVersion=2024-12-20-15:35-1"\r
+ url =None\r
+ url = "https://dmp-api.qipai.com/qp-dmp-api/ota/task?deviceId={}&sourceVersion={}".format(modem.getDevImei(),sourceVersion)\r
+ print(url)\r
+ response = request.get(url,timeout=5)\r
+ if response.status_code == 200:\r
+ data = response.json()\r
+ print("OTA升级请求:{}".format(data))\r
+ \r
+ if data:\r
+ try:\r
+ if data["code"] == 20007:#有新版本\r
+ if data["data"]["targetVersion"] != bt_versions and "fileList"in data["data"]:\r
+ #if len(data["data"]["fileList"]) == 2:\r
+ for fileList in data["data"]["fileList"]:\r
+ \r
+ if fileList["fileName"] == "update.ufw":\r
+ self.BT_ota_size = fileList["size"]\r
+ self.downloadFileVoURLBT = fileList["url"]\r
+ if data["code"] == 20006:#已是最新版本\r
+ ret_str = 0\r
+ \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ else:\r
+ print("bt_ota get error")\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+ \r
+ print("downloadFileVoURLBT{}".format(self.downloadFileVoURLBT))\r
+ print("bt_ota end") \r
+ if self.downloadFileVoURLBT !="":\r
+ battery = EventMesh.publish("get_battery")\r
+ if battery>=40:\r
+ self.ota_timer = osTimer()\r
+ #self.ota_timer.start(15000, 0, self.ota_timer_cb)\r
+ if self.ota_task_id ==None:\r
+ self.ota_task_id = _thread.start_new_thread(self.ota_task,())\r
+ ret_str = 1 \r
+ else:\r
+ ret_str = 2\r
+ return ret_str\r
+ #小系统升级,返回0,没有升级;返回,1有升级,正在执行升级;返回2,电压不够\r
+ def min_sys_ota(self,event=None,msg=None):\r
+ print("min_sys_ota")\r
+ signal = net.csqQueryPoll()\r
+ if signal == -1 or sim.getStatus() != 1:#网络不正常\r
+ print("min_sys_ota 无 SIM卡!!!!!!!!\r\n")\r
+ return 0\r
+ # return 0\r
+ response = None\r
+ ret_str =0\r
+ self.downloadFileVoURL1= ""\r
+ self.downloadFileVoURL2= ""\r
+ #self.downloadFileVoURLBT=""\r
+ try:\r
+ sourceVersion = version_util.get_unified_version()\r
+ print("sourceVersion:{}".format(sourceVersion))\r
+ url =None\r
+ url = "https://dmp-api.qipai.com/qp-dmp-api/ota/task?deviceId={}&sourceVersion={}".format(modem.getDevImei(),sourceVersion)\r
+ print(url)\r
+ response = request.get(url,timeout=5)\r
+ if response.status_code == 200:\r
+ data = response.json()\r
+ print("OTA升级请求:{}".format(data))\r
+ \r
+ if data:\r
+ try:\r
+ if data["code"] == 20007:#有新版本\r
+ if "fileList"in data["data"]:\r
+ #if len(data["data"]["fileList"]) == 2:\r
+ for fileList in data["data"]["fileList"]:\r
+ if fileList["fileName"] == "dfota_2.bin":\r
+ self.downloadFileVoURL2 = fileList["url"]\r
+ elif fileList["fileName"] == "dfota_1.bin":\r
+ self.downloadFileVoURL1 = fileList["url"]\r
+ elif fileList["fileName"] == "update.ufw":\r
+ self.BT_ota_size = fileList["size"]\r
+ self.downloadFileVoURLBT = fileList["url"]\r
+ if data["code"] == 20006:#已是最新版本\r
+ ret_str = 0\r
+ \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ else:\r
+ print("min_sys_ota get error")\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+ \r
+ print("min_sys_ota:downloadFileVoURL1:{},\ndownloadFileVoURL2:{},\ndownloadFileVoURLBT{}".format(self.downloadFileVoURL1,self.downloadFileVoURL2,self.downloadFileVoURLBT))\r
+ print("min_sys_ota end") \r
+ if self.downloadFileVoURL2!="" and self.downloadFileVoURL1!="" :\r
+ battery = EventMesh.publish("get_battery")\r
+ if battery>=40:\r
+ self.ota_timer = osTimer()\r
+ #self.ota_timer.start(15000, 0, self.ota_timer_cb)\r
+ if self.ota_task_id ==None:\r
+ self.ota_task_id = _thread.start_new_thread(self.ota_task,())\r
+ ret_str = 1 \r
+ else:\r
+ ret_str = 2\r
+ return ret_str\r
+ \r
+ # def ota_timer_cb(self, *args, **kwargs):\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.usb_event})\r
+\r
+ def ota_task(self):\r
+ import fota\r
+ utime.sleep_ms(15000)\r
+ count = 30#最多等待30秒\r
+ while 1:\r
+ count-=1\r
+ VC_satrt_flag = EventMesh.publish("get_VC_satrt_flag")#获取vc是否在运行\r
+ if VC_satrt_flag == None:\r
+ VC_satrt_flag = 0\r
+ voice_record_satrt_flag = EventMesh.publish("get_voice_record_flag")#获取静默或者语音留言是否在运行\r
+ if voice_record_satrt_flag == None:\r
+ voice_record_satrt_flag = 0\r
+ if VC_satrt_flag == 0 or count<=0 or voice_record_satrt_flag == 0:\r
+ break\r
+ utime.sleep_ms(100)\r
+ if self.downloadFileVoURLBT != "" and self.BT_ota_size != 0:\r
+ count = 5 #重试5次\r
+ while 1:\r
+ count -=1\r
+ ret = EventMesh.publish("bt_ota_run",[self.downloadFileVoURLBT,self.BT_ota_size ]) #升级蓝牙\r
+\r
+ if ret == 0 or count <=0:\r
+ break\r
+ utime.sleep_ms(1000)\r
+ utime.sleep_ms(1000)\r
+ #EventMesh.publish("rest_TB") #复位蓝牙\r
+ utime.sleep_ms(50000)\r
+ if self.downloadFileVoURL1 == "" and self.downloadFileVoURL2 == "": #cat1没有升级的时候,要重启设备\r
+ Power.powerRestart()\r
+ if self.downloadFileVoURL1 != "" and self.downloadFileVoURL2 != "":\r
+ fota_obj = fota() \r
+ fota_obj.httpDownload(url1=self.downloadFileVoURL1,url2=self.downloadFileVoURL2)\r
+ #ota升级结果返回,开机时调用一次即可\r
+ def ota_result(self,event=None,msg=None):\r
+ response = None\r
+ try:\r
+ currentVersion = version_util.get_unified_version()\r
+ print("currentVersion:{}".format(currentVersion))\r
+ url = (constants.DMP_BASE + "/ota/result?deviceId={}¤tVersion={}").format(modem.getDevImei(),currentVersion)\r
+ print(url)\r
+ response = request.post(url,timeout=5)\r
+ if response.status_code == 200:\r
+ data = response.json()\r
+ print("OTA升级结果上报:{}".format(data))\r
+ else:\r
+ print("ota_result error!!!!")\r
+ for i in response.text:\r
+ print(i)\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+ #ICCID上传\r
+ def pots_ICCID(self,event=None,msg=None):\r
+ response = None\r
+ try:\r
+ iccid = sim.getIccid()\r
+ if iccid != -1 and len(iccid) > 5:\r
+ print("pots_ICCID:{}".format(iccid))\r
+ #Headers = {'Transfer-Encodong':'chunked'}\r
+ token = EventMesh.publish("get_dev_token")\r
+ headers_T = None\r
+ if token:\r
+ headers_T = {"bz-dt":"{}".format(token)}\r
+ #http://dmp.raisound.com/bz-api/dev/iccid?deviceId=866610001099679&iccid=89860002192307889057\r
+ url =None\r
+ url = (constants.DMP_BASE + "/dev/iccid?deviceId={}&iccid={}").format(modem.getDevImei(),iccid)\r
+ print("{} H:{}".format(url,headers_T))\r
+ response = request.post(url,headers = headers_T,timeout=5)\r
+ if response.status_code == 200:\r
+ data = response.json()\r
+ print("pots_ICCID:{}".format(data))\r
+ else:\r
+ print("pots_ICCID error!!!!")\r
+ for i in response.text:\r
+ print(i)\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+# 原 VC_State_* 整型已移除;会话/轮次见 VCTurnState / VCStateMachine(含 tts_rx_phase、speech_uplink_open)。\r
+\r
+VC_record_TYPE_PCM = 0\r
+VC_record_TYPE_AMR = 1\r
+VC_play_TYPE_PCM = 0\r
+VC_play_TYPE_MP3 = 1\r
+\r
+#websocket发送数据编号\r
+# AI_client_asr_num = 0\r
+# AI_client_chat_num = 1\r
+# AI_client_tts_num = 2\r
+AI_client_VC_num = 3\r
+AI_client_asr_num = 4\r
+\r
+\r
+class VCSessionState:\r
+ Disconnected = 0\r
+ Connecting = 1\r
+ Starting = 2\r
+ Started = 3\r
+\r
+\r
+class VCTurnState:\r
+ Idle = 0\r
+ Listening = 1\r
+ SpeechStreaming = 2\r
+ SpeechEnded = 3\r
+ Thinking = 4\r
+ TtsReceiving = 5\r
+ Playing = 6\r
+ AwaitPlayedAck = 7\r
+\r
+\r
+class VC_TtsRxPhase:\r
+ Idle = 0\r
+ Started = 1\r
+ ServerCompleted = 2\r
+\r
+\r
+class VCMode:\r
+ Realtime = 0 # VAD on(长对话,协议:无需 audio.create/completed)\r
+ PushToTalk = 1 # VAD off(单对话,协议:必须 audio.create/completed 包裹音频帧)\r
+\r
+\r
+class VCStateMachine:\r
+ # 轻量状态机骨架(先落地骨架与动作接口,后续逐步把旧 if/flag 逻辑收敛进来)。\r
+ #\r
+ # 约束来自《金三服务-语音对话V2.5》:\r
+ # - 收到 started 后才允许发音频帧(否则 session not initialized)\r
+ # - VAD 开启:不需要 audio.create/completed\r
+ # - VAD 关闭:必须 audio.create → frames → audio.completed\r
+ # - 播放结束 audio.played 是进入下一轮的门控信号之一(尤其关键词打断场景)\r
+\r
+ def __init__(self, vc):\r
+ self.vc = vc\r
+ self.session = VCSessionState.Disconnected\r
+ self.turn = VCTurnState.Idle\r
+ self.last_llm_ms = 0\r
+ self.last_tts_pending_ms = 0\r
+ self.await_played = False\r
+ self.last_tts_completed_ms = 0\r
+ self.tts_rx_phase = VC_TtsRxPhase.Idle\r
+ self.speech_uplink_open = 0\r
+\r
+ def reset_rx_phases(self):\r
+ self.tts_rx_phase = VC_TtsRxPhase.Idle\r
+ self.speech_uplink_open = 0\r
+\r
+ def mark_tts_rx_idle(self):\r
+ self.tts_rx_phase = VC_TtsRxPhase.Idle\r
+\r
+ def on_server_asr_started(self):\r
+ if self.session == VCSessionState.Started:\r
+ self.speech_uplink_open = 1\r
+ self.log("server.asr.started")\r
+\r
+ def _turn_name(self, t):\r
+ try:\r
+ return {\r
+ VCTurnState.Idle: "Idle",\r
+ VCTurnState.Listening: "Listening",\r
+ VCTurnState.SpeechStreaming: "SpeechStreaming",\r
+ VCTurnState.SpeechEnded: "SpeechEnded",\r
+ VCTurnState.Thinking: "Thinking",\r
+ VCTurnState.TtsReceiving: "TtsReceiving",\r
+ VCTurnState.Playing: "Playing",\r
+ VCTurnState.AwaitPlayedAck: "AwaitPlayedAck",\r
+ }.get(t, str(t))\r
+ except Exception:\r
+ return str(t)\r
+\r
+ def _session_name(self, s):\r
+ try:\r
+ return {\r
+ VCSessionState.Disconnected: "Disconnected",\r
+ VCSessionState.Connecting: "Connecting",\r
+ VCSessionState.Starting: "Starting",\r
+ VCSessionState.Started: "Started",\r
+ }.get(s, str(s))\r
+ except Exception:\r
+ return str(s)\r
+\r
+ def log(self, reason=""):\r
+ # 只用于关键节点的单行状态日志,便于串口快速定位状态漂移\r
+ try:\r
+ print("[FSM] {} session={} turn={} await_played={} tts_rx={} speech_up={}".format(\r
+ reason,\r
+ self._session_name(self.session),\r
+ self._turn_name(self.turn),\r
+ 1 if self.await_played else 0,\r
+ int(self.tts_rx_phase),\r
+ 1 if self.speech_uplink_open else 0,\r
+ ))\r
+ except Exception:\r
+ pass\r
+\r
+ def mode(self):\r
+ try:\r
+ return VCMode.Realtime if self.vc.VC_mode == 0 else VCMode.PushToTalk\r
+ except Exception:\r
+ return VCMode.Realtime\r
+\r
+ def on_session_start_request(self):\r
+ self.session = VCSessionState.Starting\r
+ self.turn = VCTurnState.Idle\r
+ # 新会话开始时必须清空上一轮“待播完”状态,否则会出现:\r
+ # - 切换模式/重连后 await_played 仍为 True → play_end 逻辑抢跑/不触发 → 无法自动开麦\r
+ self.await_played = False\r
+ self.last_tts_completed_ms = 0\r
+ self.last_llm_ms = 0\r
+ self.reset_rx_phases()\r
+\r
+ def on_server_started(self):\r
+ self.reset_rx_phases()\r
+ self.session = VCSessionState.Started\r
+ # 新会话 started:确保进入 Listening,且清理上轮播放门控\r
+ self.await_played = False\r
+ self.last_tts_completed_ms = 0\r
+ if self.turn in (VCTurnState.Idle, VCTurnState.AwaitPlayedAck):\r
+ self.turn = VCTurnState.Listening\r
+ self.log("server.started")\r
+\r
+ def on_server_vad_silence(self):\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.SpeechEnded\r
+ self.log("server.vad.silence")\r
+\r
+ def on_server_llm_created(self):\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.Thinking\r
+ try:\r
+ self.last_llm_ms = utime.ticks_ms()\r
+ except Exception:\r
+ self.last_llm_ms = 1\r
+ self.log("server.llm.created")\r
+\r
+ def on_server_tts_started(self):\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.TtsReceiving\r
+ self.tts_rx_phase = VC_TtsRxPhase.Started\r
+ self.log("server.tts.started")\r
+\r
+ def on_server_tts_completed(self):\r
+ if self.session == VCSessionState.Started:\r
+ # tts.completed 只表示服务端音频下发结束,本地可能仍在播放;保持 Playing,由 await_played 门控播完上报\r
+ self.await_played = True\r
+ self.tts_rx_phase = VC_TtsRxPhase.ServerCompleted\r
+ try:\r
+ self.last_tts_completed_ms = utime.ticks_ms()\r
+ except Exception:\r
+ self.last_tts_completed_ms = 1\r
+ self.log("server.tts.completed")\r
+\r
+ def on_server_canceled(self):\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.Listening\r
+ self.reset_rx_phases()\r
+ self.log("server.canceled")\r
+\r
+ def on_server_completed(self):\r
+ if self.session == VCSessionState.Started:\r
+ # 本轮结束:默认回到 Listening;是否自动开麦由 action 门控决定\r
+ # 若仍在等待本地播完(await_played),completed 不应把 turn 拉回 Listening,\r
+ # 否则会触发 completed→uplink_start 抢跑,打断播放链路。\r
+ if not self.await_played and self.turn != VCTurnState.AwaitPlayedAck:\r
+ self.turn = VCTurnState.Listening\r
+ self.log("server.completed")\r
+\r
+ def on_server_event(self, event, jsload_data=None):\r
+ # 事件→动作(action list)适配层。\r
+ # 现阶段只产出“可幂等的控制动作”(uplink start/stop),避免和旧逻辑重复触发播放等副作用。\r
+ # 后续会逐步把更多行为(play_start/audio.played/ptt create/completed)迁移到这里。\r
+ actions = []\r
+ try:\r
+ # LLM 阶段必须驱动 FSM.turn,否则 turn 会停在 SpeechEnded,导致:\r
+ # - _fsm_timeout_table_tick 里「Thinking 超时 / synthesis 超时」永不命中\r
+ # - 与注释和 force_tts 前置条件不一致\r
+ if event == "llm.created":\r
+ self.on_server_llm_created()\r
+ return actions\r
+ if event == "llm.completed":\r
+ # 若对端偶发跳过 llm.created,用 completed 把 turn 补到 Thinking,避免兜底全失效\r
+ try:\r
+ if self.session == VCSessionState.Started:\r
+ if self.turn in (VCTurnState.SpeechEnded, VCTurnState.Listening):\r
+ self.turn = VCTurnState.Thinking\r
+ except Exception:\r
+ pass\r
+ return actions\r
+ if event == "started":\r
+ self.on_server_started()\r
+ return actions\r
+ if event == "vad.silence":\r
+ self.on_server_vad_silence()\r
+ actions.append(("uplink_stop", "vad.silence"))\r
+ return actions\r
+ if event == "tts.started":\r
+ self.on_server_tts_started()\r
+ actions.append(("uplink_stop", "tts.started"))\r
+ actions.append(("play_start", "tts.started"))\r
+ return actions\r
+ if event == "tts.completed":\r
+ self.on_server_tts_completed()\r
+ # 播放可能已在 started 时开启,这里不再强制 play_start,避免反复 open PCM\r
+ return actions\r
+ if event == "canceled":\r
+ self.on_server_canceled()\r
+ # canceled:仅长对话允许“立刻回到聆听并尝试开麦”(由 _uplink_request_start 门控保证安全幂等)\r
+ try:\r
+ if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0:\r
+ actions.append(("uplink_start", "canceled"))\r
+ except Exception:\r
+ pass\r
+ return actions\r
+ if event == "completed":\r
+ self.on_server_completed()\r
+ # completed:若不在等待 audio.played(说明本轮无 TTS),仅长对话可直接进入下一轮聆听\r
+ try:\r
+ if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0:\r
+ if (not getattr(self, "await_played", False)) and self.turn != VCTurnState.AwaitPlayedAck:\r
+ actions.append(("uplink_start", "completed"))\r
+ except Exception:\r
+ pass\r
+ return actions\r
+ except Exception:\r
+ pass\r
+ return actions\r
+\r
+ def on_local_audio_played(self):\r
+ # 本地播报完成后:长对话回到 Listening,等待下一轮输入\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.Listening\r
+ self.await_played = False\r
+ self.last_tts_completed_ms = 0\r
+ self.mark_tts_rx_idle()\r
+ self.log("local.audio_played")\r
+\r
+ def on_local_play_start(self):\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.Playing\r
+ self.log("local.play_start")\r
+\r
+ def on_local_synthesis_sent(self):\r
+ # synthesis 发出后到 tts.started 之间:视为 TTS 接收阶段,禁止开麦\r
+ if self.session == VCSessionState.Started:\r
+ self.turn = VCTurnState.TtsReceiving\r
+ self.log("local.synthesis_sent")\r
+\r
+ def on_local_play_end(self):\r
+ actions = []\r
+ try:\r
+ if self.session != VCSessionState.Started:\r
+ return actions\r
+ if self.await_played:\r
+ actions.append(("send_audio_played", "local.play_end"))\r
+ # 长对话模式:对话过程中的 TTS 播完后,自动开麦继续当前对话。\r
+ # 例外:\r
+ # - 刚切到长对话模式的提示音(mode_switch_long)不应自动开麦\r
+ # - 其它提示/提醒(prompt)不应自动开麦\r
+ # 实际开麦是否允许由 _uplink_request_start 的门控(FSM.turn / record_state 等)保证安全幂等。\r
+ try:\r
+ if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0:\r
+ k = getattr(self.vc, "_last_tts_kind", "") or ""\r
+ if k == "dialog":\r
+ # 用户短按打断(RequestToSpeak):等 canceled 再开麦,勿 play_end 抢跑\r
+ if getattr(self.vc, "_vc_pending_interrupt_uplink", 0) != 1:\r
+ actions.append(("uplink_start", "local.play_end"))\r
+ except Exception:\r
+ pass\r
+ self.on_local_audio_played()\r
+ except Exception:\r
+ pass\r
+ self.log("local.play_end")\r
+ return actions\r
+\r
+\r
+class AI_VC(Abstract):\r
+ def get_vc_tts_playback_active(self, *args, **kwargs):\r
+ try:\r
+ return 1 if self._vc_tts_playback_active() else 0\r
+ except Exception:\r
+ return 0\r
+\r
+ def get_vc_stream_active(self, *args, **kwargs):\r
+ # 供本地提示音侧查询:TTS 流播放期间本地不可抢占。\r
+ try:\r
+ if self._vc_tts_playback_active():\r
+ return 1\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self.fsm, "await_played", False):\r
+ return 1\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:\r
+ return 1\r
+ except Exception:\r
+ pass\r
+ try:\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):\r
+ return 1\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "_tts_pending", 0) == 1:\r
+ return 1\r
+ except Exception:\r
+ pass\r
+ return 0\r
+\r
+ def _vc_fsm_log_snip(self):\r
+ try:\r
+ f = self.fsm\r
+ return "sess={} turn={} tts_rx={} await={} sp_up={}".format(\r
+ int(getattr(f, "session", -1)),\r
+ int(getattr(f, "turn", -1)),\r
+ int(getattr(f, "tts_rx_phase", -1)),\r
+ 1 if getattr(f, "await_played", False) else 0,\r
+ 1 if getattr(f, "speech_uplink_open", 0) else 0,\r
+ )\r
+ except Exception:\r
+ return "fsm=?"\r
+\r
+ def _vc_tts_pipeline_busy(self):\r
+ try:\r
+ if getattr(self.fsm, "await_played", False):\r
+ return True\r
+ if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:\r
+ return True\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):\r
+ return True\r
+ if getattr(self, "_tts_pending", 0) == 1:\r
+ return True\r
+ if getattr(self, "VC_Play_flag", 0) == 1:\r
+ return True\r
+ if self._vc_local_play_busy():\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _vc_local_play_busy(self):\r
+ try:\r
+ if getattr(self, "_vc_local_play_active", 0) != 1:\r
+ return False\r
+ until = getattr(self, "_vc_local_play_until_ms", 0)\r
+ if until and utime.ticks_diff(until, utime.ticks_ms()) > 0:\r
+ return True\r
+ self._vc_local_play_active = 0\r
+ self._vc_local_play_until_ms = 0\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _vc_capture_allowed(self, reason=""):\r
+ """采集/上行开麦前:下行播放或本地短音未结束则禁止。"""\r
+ try:\r
+ if self._vc_tts_pipeline_busy():\r
+ return False\r
+ if self._vc_local_play_busy():\r
+ return False\r
+ except Exception:\r
+ return False\r
+ return True\r
+\r
+ def _vc_stop_downlink_for_interrupt(self, reason="interrupt"):\r
+ """详设 DES-SCN-003:打断/取消时先本地停播清队列,再允许开麦。"""\r
+ try:\r
+ if getattr(self, "AI_audio_queue", None) is not None:\r
+ if self.AI_audio_queue.empty() != True:\r
+ self.AI_audio_queue.clear()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._vc_tts_pipeline_busy() or getattr(self, "is_ruppt_tts", 0) == 1:\r
+ self._drop_tts_audio = 1\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._ptt_hard_mute_local_playback()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:\r
+ self.VC_Play_End()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._vc_local_play_active = 0\r
+ self._vc_local_play_until_ms = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.fsm.await_played = False\r
+ self._need_audio_played_ack = 0\r
+ self._tts_pending = 0\r
+ self._tts_pending_ms = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ pending_intr = getattr(self, "_vc_pending_interrupt_uplink", 0) == 1\r
+ if (\r
+ not pending_intr\r
+ and getattr(self, "VC_record_state_flag", 0) != 0\r
+ and getattr(self, "record_data_save_flag", 0) == 0\r
+ ):\r
+ self._uplink_request_stop("stop_downlink/" + str(reason))\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_local_play(self, path, play_mode=1, loop=1, hold_ms=650):\r
+ """本地 MP3 短音:播前停录/停流式 TTS,避免与麦克风采集叠用。"""\r
+ try:\r
+ if getattr(self, "record_data_save_flag", 0) == 0 and getattr(self, "VC_record_state_flag", 0) != 0:\r
+ self._uplink_request_stop("local_play")\r
+ utime.sleep_ms(30)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._vc_tts_playback_active():\r
+ self._ptt_hard_mute_local_playback()\r
+ try:\r
+ self.VC_Play_End()\r
+ except Exception:\r
+ pass\r
+ except Exception:\r
+ pass\r
+ try:\r
+ hold = int(hold_ms)\r
+ if hold < 200:\r
+ hold = 200\r
+ self._vc_local_play_active = 1\r
+ self._vc_local_play_until_ms = utime.ticks_add(utime.ticks_ms(), hold)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.aud.play(play_mode, loop, path)\r
+ except Exception as e:\r
+ try:\r
+ self._vc_local_play_active = 0\r
+ self._vc_local_play_until_ms = 0\r
+ except Exception:\r
+ pass\r
+ usys.print_exception(e)\r
+\r
+ def _vc_tts_playback_active(self):\r
+ """仅服务端 TTS/PCM 流在播(VC_Play_Start);aud.play 本地短音不算。"""\r
+ try:\r
+ if getattr(self, "VC_Play_flag", 0) == 1:\r
+ return True\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "play_pcm", None) is not None:\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _vc_vad_uplink_stop_eligible(self):\r
+ # 对应旧 VC_state >= SpeechStart\r
+ try:\r
+ if getattr(self.fsm, "speech_uplink_open", 0):\r
+ return True\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ return t not in (VCTurnState.Idle, VCTurnState.Listening)\r
+ except Exception:\r
+ return False\r
+\r
+ def _vc_long_idle_active_speech_or_later(self):\r
+ # 对应旧 VC_state >= SpeechIng\r
+ try:\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ return t not in (VCTurnState.Idle, VCTurnState.Listening)\r
+ except Exception:\r
+ return False\r
+\r
+ def _vc_is_business_idle(self):\r
+ # Level2:LED ai_state 为 idle,且无录音/流播/音乐闹钟等(与 WS ping 无关)\r
+ try:\r
+ if getattr(self, "music_play_flag", 0) == 1:\r
+ return False\r
+ if getattr(self, "alarm_play_flag", 0) == 1:\r
+ return False\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ return False\r
+ if getattr(self, "VC_Play_flag", 0) != 0:\r
+ return False\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if EventMesh.publish("get_vc_stream_active") == 1:\r
+ return False\r
+ except Exception:\r
+ pass\r
+ try:\r
+ st = EventMesh.publish("get_led_debug_state")\r
+ if st and isinstance(st, dict):\r
+ if (st.get("ai") or "") != "idle":\r
+ return False\r
+ except Exception:\r
+ return False\r
+ return True\r
+\r
+ def _vc_idle_deep_sleep_impl(self):\r
+ try:\r
+ _need_close = (self.VC_satrt_flag == 1) or (getattr(self, "AI_client_VC_isconnect", 0) == 1)\r
+ print("[VC][idle_L2] 触发休眠流程 need_Close={} VC_mode={}".format(\r
+ 1 if _need_close else 0, getattr(self, "VC_mode", -1)))\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self.VC_satrt_flag == 1 or getattr(self, "AI_client_VC_isconnect", 0) == 1:\r
+ try:\r
+ print("[VC][idle_L2] 调用 Close 断开会话/链路")\r
+ except Exception:\r
+ pass\r
+ self.Close()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ utime.sleep_ms(100)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ print("[VC][idle_L2] 发布 Hsensor_set_sleep")\r
+ EventMesh.publish("Hsensor_set_sleep")\r
+ except Exception:\r
+ pass\r
+ try:\r
+ print("[VC][idle_L2] 发布 SYSTEM_SLEEP ({})".format(constants.EVENT.SYSTEM_SLEEP))\r
+ EventMesh.publish(constants.EVENT.SYSTEM_SLEEP)\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_rx_resets_long_idle_timer(self, recv_data):\r
+ """是否应因本帧下行清零 close_timerout(排除保活/心跳,含 V2.5 文本 ping 对端应答)。"""\r
+ try:\r
+ if recv_data is None:\r
+ return False\r
+ if isinstance(recv_data, (bytes, bytearray)):\r
+ return True\r
+ if not isinstance(recv_data, str):\r
+ return False\r
+ t = recv_data.strip()\r
+ if not t:\r
+ return False\r
+ tl = t.lower()\r
+ if tl in ("ping", "pong"):\r
+ return False\r
+ try:\r
+ jd = ujson.loads(t)\r
+ except Exception:\r
+ return True\r
+ ev = jd.get("event")\r
+ if ev is None or ev == "":\r
+ ev = jd.get("type")\r
+ if ev is None:\r
+ return True\r
+ if str(ev).strip().lower() in _VC_KEEPALIVE_RX_EVENTS:\r
+ return False\r
+ return True\r
+ except Exception:\r
+ return True\r
+\r
+ # -------------------------------------------------------------------------\r
+ # 电源键 / AI 键:按 VC_mode 拆函数,便于阅读与修改(长对话 vs 单对话 PTT)\r
+ # -------------------------------------------------------------------------\r
+ def _handle_pwk_short_release(self, event=None, msg=None, merged_ai_tail=False):\r
+ """电源键短按:读留言标志、退会议/翻译;否则 Close,或单键模式下走 AI_key_release。"""\r
+ print("pwk_key_release")\r
+ if EventMesh.publish("get_recv_voice") == 1:\r
+ EventMesh.publish("update_play_new_voice_info")\r
+ self.recv_voice = 0\r
+ EventMesh.publish("set_history_voice_flag", 0)\r
+ self.AI_new_voice_url = ""\r
+ if self.trans_flag == 1 or self.meet_flag == 1:\r
+ EventMesh.publish("exit_meet_mode")\r
+ EventMesh.publish("exit_trans_mode")\r
+ return\r
+ if merged_ai_tail:\r
+ self.AI_key_release(event, msg)\r
+ else:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
+ EventMesh.publish("stop_sos_call")\r
+\r
+ def _ai_key_press_ptt_mode(self):\r
+ """单对话 PTT:短按仅停 TTS(不换 sid);非播放态短按吞掉;长按由 AI_key_long_press→ptt_begin。"""\r
+ if self.VC_mode != 1:\r
+ return False\r
+ try:\r
+ playing = False\r
+ try:\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ playing = t in (VCTurnState.TtsReceiving, VCTurnState.Playing)\r
+ except Exception:\r
+ playing = False\r
+ if playing or self._vc_tts_pipeline_busy():\r
+ self._ptt_short_press_stop = 1\r
+ try:\r
+ self._ptt_hard_mute_local_playback()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.VC_Play_End()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.fsm.await_played = False\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._ptt_led_lock_listening = 0\r
+ self._ptt_interrupt_active = 0\r
+ self.is_ruppt_tts = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._vc_publish_led("idle", "ptt_short_press/stop_tts")\r
+ except Exception:\r
+ pass\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return True\r
+\r
+ def _ai_key_press_long_dialogue_interrupt(self):\r
+ """长对话:TTS/播放忙或 turn==Thinking 时短按 → RequestToSpeak(先 cancel,canceled 后再开麦)。"""\r
+ if self.VC_mode != 0:\r
+ return False\r
+ try:\r
+ if getattr(self, "AI_client_VC_isconnect", 0) != 1 or getattr(self, "_vc_session_started", 0) != 1:\r
+ return False\r
+ except Exception:\r
+ return False\r
+ need_cancel = False\r
+ if self._vc_tts_pipeline_busy():\r
+ need_cancel = True\r
+ else:\r
+ try:\r
+ if getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking:\r
+ need_cancel = True\r
+ except Exception:\r
+ pass\r
+ if not need_cancel:\r
+ return False\r
+ print("[KEY] long interrupt -> RequestToSpeak (cancel then uplink)")\r
+ self.is_ruppt_tts = 1\r
+ try:\r
+ self._vc_pending_interrupt_uplink = 1\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})\r
+ return True\r
+\r
+ def _ai_key_press_long_dialogue_wake(self):\r
+ """长对话未连接或未 started:直接 AI_VC_start,勿 Close+sleep。"""\r
+ if self.VC_mode != 0:\r
+ return False\r
+ if getattr(self, "voice_flag", 0) == 1 or getattr(self, "meet_flag", 0) == 1 or getattr(self, "trans_flag", 0) == 1:\r
+ return False\r
+ try:\r
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
+ return False\r
+ except Exception:\r
+ pass\r
+ print("[KEY] long wake AI_VC_start (no Close)")\r
+ try:\r
+ self.Close_flag = 0\r
+ except Exception:\r
+ pass\r
+ self.get_config_timer_count = 0\r
+ self.key_state = 1\r
+ self.AI_TTS_str = ""\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ self.AI_VC_start()\r
+ self.AI_key_long_press_flag = 0\r
+ return True\r
+\r
+ def _vc_led_alert_rb_flash(self):\r
+ try:\r
+ EventMesh.publish("set_led_state", {"alert": "rb_flash_2"})\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_clear_stale_record_state(self, reason=""):\r
+ """record_state_flag 非 0 但可能无真实录音时先停录复位,避免 uplink 门控拒开麦。"""\r
+ try:\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ self.VC_Speech_End()\r
+ except Exception:\r
+ try:\r
+ self.VC_record_state_flag = 0\r
+ except Exception:\r
+ pass\r
+\r
+ def _ai_key_press_long_dialogue_when_session_ready(self):\r
+ """长对话已连接且 session 已 started:非播放态直接开麦续聊(勿 idle 时发 cancel)。"""\r
+ if self.VC_mode != 0:\r
+ return False\r
+ try:\r
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
+ # 模式切换提示音会置 Close_flag=1;用户再短按须恢复可开麦\r
+ try:\r
+ self.Close_flag = 0\r
+ except Exception:\r
+ pass\r
+ self._vc_clear_stale_record_state("key.press/continue")\r
+ self.request_uplink_start("key.press/continue")\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _ai_key_release_handle_ptt(self):\r
+ """单对话 PTT 松手:仅在上行录音中时 end_speech;禁止误触发 AI_VC_start(否则新 sid / cancel 竞态)。"""\r
+ if self.AI_key_long_press_flag == 1:\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ try:\r
+ gap = 200\r
+ if "vc_config" in globals() and vc_config:\r
+ gap = int(vc_config.get_int("PTT_RELEASE_END_SPEECH_DELAY_MS", 200))\r
+ if gap > 0:\r
+ utime.sleep_ms(gap)\r
+ except Exception:\r
+ pass\r
+ print("[PTT] release end_speech asr_str={!r} rec={}".format(\r
+ self.AI_ASR_str, getattr(self, "VC_record_state_flag", -1)))\r
+ self.AI_client_VC_end_speech()\r
+ else:\r
+ self.AI_key_long_press_flag = 0\r
+ try:\r
+ if getattr(self, "_ptt_short_press_stop", 0) == 1:\r
+ self._ptt_short_press_stop = 0\r
+ except Exception:\r
+ pass\r
+ print("[PTT] release_no_uplink_skip")\r
+ return\r
+ try:\r
+ if getattr(self, "_ptt_short_press_stop", 0) == 1:\r
+ self._ptt_short_press_stop = 0\r
+ except Exception:\r
+ pass\r
+ print("[PTT] release_short_no_long")\r
+\r
+ def _ai_key_long_press_route(self, event=None, msg=None):\r
+ """长按:翻译先 Close 再开翻译录音;单对话→ptt_begin;长对话→与短按一致走 AI_key_press 链路。"""\r
+ if self.trans_flag == 1:\r
+ self.Close()\r
+ EventMesh.publish("start_voice_record", 4)\r
+ self.AI_key_long_press_flag = 1\r
+ if self.VC_mode == 1:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.ptt_begin})\r
+ else:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})\r
+\r
+ # ===== Push-To-Talk(单对话模式)=====\r
+ # 语义:\r
+ # - 长按:无论当前处于 TTS/Thinking/任何中间态,立即打断并进入新一轮录音上行\r
+ # - 松开:停止录音并等待服务端返回(由 AI_client_VC_end_speech 触发)\r
+ def _ptt_end_led_suppress(self):\r
+ # 保持 _vc_play_end_suppress_idle_led / _ptt_led_lock_listening 直至真开录(VC_Speech_Start)\r
+ pass\r
+\r
+ def _ptt_should_drop_inbound_tts(self):\r
+ """单对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。"""\r
+ try:\r
+ if getattr(self, "VC_mode", -1) != 1:\r
+ return False\r
+ if getattr(self, "_drop_tts_audio", 0) == 1:\r
+ return True\r
+ if getattr(self, "is_ruppt_tts", 0) == 1:\r
+ return True\r
+ if getattr(self, "_ptt_interrupt_active", 0) == 1:\r
+ return True\r
+ if getattr(self, "_vc_serial_canceling", 0) == 1:\r
+ return True\r
+ if getattr(self, "_ptt_pending_arm", 0) == 1:\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _ptt_should_skip_remote_playback(self):\r
+ return self._ptt_should_drop_inbound_tts()\r
+\r
+ def _ptt_enter_interrupt_arm(self):\r
+ try:\r
+ self._ptt_arm_epoch = int(getattr(self, "_ptt_arm_epoch", 0)) + 1\r
+ self._ptt_interrupt_active = 1\r
+ self._ptt_led_lock_listening = 1\r
+ self._vc_play_end_suppress_idle_led = 1\r
+ self._drop_tts_audio = 1\r
+ self.is_ruppt_tts = 1\r
+ except Exception:\r
+ pass\r
+\r
+ def _ptt_abort_interrupt_arm(self):\r
+ try:\r
+ self._ptt_interrupt_active = 0\r
+ self._ptt_led_lock_listening = 0\r
+ self._vc_play_end_suppress_idle_led = 0\r
+ self._drop_tts_audio = 0\r
+ self.is_ruppt_tts = 0\r
+ except Exception:\r
+ pass\r
+\r
+ def _ptt_on_record_hardware_started(self):\r
+ """真开录后:允许新 TTS、放开灯锁;旧 TTS 已在打断期丢弃。"""\r
+ try:\r
+ self._ptt_interrupt_active = 0\r
+ self._ptt_led_lock_listening = 0\r
+ self._vc_play_end_suppress_idle_led = 0\r
+ self._drop_tts_audio = 0\r
+ self.is_ruppt_tts = 0\r
+ except Exception:\r
+ pass\r
+\r
+ def _ptt_hard_mute_local_playback(self):\r
+ try:\r
+ self.VC_Play_flag = 0\r
+ if getattr(self, "VC_play_TYPE", None) == VC_play_TYPE_PCM:\r
+ if getattr(self, "play_pcm", None) is not None:\r
+ try:\r
+ self.play_pcm.stream_stop()\r
+ self.play_pcm.close()\r
+ except Exception:\r
+ pass\r
+ self.play_pcm = None\r
+ try:\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ except Exception:\r
+ pass\r
+ if getattr(self, "AI_audio_queue", None) is not None:\r
+ try:\r
+ if self.AI_audio_queue.empty() != True:\r
+ self.AI_audio_queue.clear()\r
+ except Exception:\r
+ pass\r
+ except Exception:\r
+ pass\r
+ try:\r
+ gap = 0\r
+ if "vc_config" in globals() and vc_config:\r
+ gap = int(vc_config.get_int("PTT_POST_MUTE_MS", 30))\r
+ if gap > 0:\r
+ utime.sleep_ms(gap)\r
+ except Exception:\r
+ pass\r
+\r
+ def battery_priority_mute_vc(self, event=None, msg=None):\r
+ """低电/关机提示音:短暂停 VC TTS,避免被流式播放挡住。"""\r
+ try:\r
+ self._ptt_hard_mute_local_playback()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:\r
+ self.VC_Play_End()\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_enqueue_btn(self, func, data=None):\r
+ """统一经 publish_AI_btnInfo 入队,在 btnInfo_queue_thread 串行执行。"""\r
+ try:\r
+ if data is not None:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": func, "data": data})\r
+ else:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": func})\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_ptt_cancel_wait_ms(self):\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ return int(vc_config.get_int("PTT_CANCEL_WAIT_MS", 2500))\r
+ except Exception:\r
+ pass\r
+ return 2500\r
+\r
+ def _vc_ptt_need_interrupt_wait(self):\r
+ """单对话:服务端仍在思考/收 TTS/播报时,开录前须先 cancel 并等 canceled。"""\r
+ try:\r
+ if getattr(self, "VC_mode", -1) != 1:\r
+ return False\r
+ turn = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ if self._vc_tts_pipeline_busy():\r
+ return True\r
+ if turn in (\r
+ VCTurnState.Thinking, VCTurnState.TtsReceiving,\r
+ VCTurnState.Playing, VCTurnState.AwaitPlayedAck,\r
+ ):\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _vc_begin_ptt_cancel_wait(self, reason="ptt_interrupt"):\r
+ try:\r
+ self._ptt_pending_arm = 1\r
+ self._vc_serial_canceling = 1\r
+ try:\r
+ self._vc_cancel_deadline_ms = utime.ticks_add(\r
+ utime.ticks_ms(), self._vc_ptt_cancel_wait_ms())\r
+ except Exception:\r
+ self._vc_cancel_deadline_ms = 0\r
+ json_data = {"event": "cancel", "payload": {}}\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ self._drop_tts_audio = 1\r
+ print("[VC][serial] cancel_wait reason={} turn={}".format(\r
+ reason, getattr(getattr(self, "fsm", None), "turn", "?")))\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_on_ptt_cancel_settled(self, via="canceled"):\r
+ """btn 线程:cancel 已收尾,执行挂起的 PTT 开录。"""\r
+ try:\r
+ if not getattr(self, "_vc_serial_canceling", 0) and not getattr(self, "_ptt_pending_arm", 0):\r
+ return\r
+ self._vc_serial_canceling = 0\r
+ self._vc_cancel_deadline_ms = 0\r
+ if getattr(self, "_ptt_pending_arm", 0):\r
+ self._ptt_pending_arm = 0\r
+ print("[VC][serial] ptt_arm after {}".format(via))\r
+ self._ptt_arm_uplink()\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_tick_serial_cancel_timeout(self):\r
+ if not getattr(self, "_vc_serial_canceling", 0):\r
+ return\r
+ try:\r
+ dl = getattr(self, "_vc_cancel_deadline_ms", 0)\r
+ if dl and utime.ticks_diff(utime.ticks_ms(), dl) >= 0:\r
+ print("[VC][serial] cancel_wait timeout -> arm")\r
+ self._vc_on_ptt_cancel_settled(via="timeout")\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_fsm_dispatch_cb(self, data):\r
+ """btn 线程:处理服务端 FSM 事件(单对话串行)。"""\r
+ try:\r
+ ev, jsload = data\r
+ except Exception:\r
+ return\r
+ try:\r
+ actions = self.fsm.on_server_event(ev, jsload)\r
+ self._fsm_apply_actions(actions)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if ev == "canceled":\r
+ self._vc_on_ptt_cancel_settled(via="canceled")\r
+\r
+ def _vc_dispatch_server_event(self, ev, jsload_data=None):\r
+ try:\r
+ if ev == "canceled":\r
+ self._vc_stop_downlink_for_interrupt("canceled")\r
+ except Exception:\r
+ pass\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ self._vc_enqueue_btn(self._vc_fsm_dispatch_cb, (ev, jsload_data))\r
+ else:\r
+ try:\r
+ self._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))\r
+ except Exception:\r
+ pass\r
+\r
+ def _ptt_arm_uplink(self, t0=0):\r
+ """btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""\r
+ try:\r
+ self.is_ruppt_tts = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._ptt_audio_created = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.AI_ASR_flag = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.AI_client_VC_send_speech()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.VC_Speech_Start_now()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._vc_publish_led("listening", "ptt_begin/recording")\r
+ except Exception:\r
+ pass\r
+ self._ptt_end_led_suppress()\r
+ try:\r
+ if t0:\r
+ print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))\r
+ except Exception:\r
+ pass\r
+\r
+ def ptt_begin(self, reason="key.long_press"):\r
+ try:\r
+ if self.VC_mode != 1:\r
+ return\r
+ except Exception:\r
+ return\r
+\r
+ # PTT 新一轮开始:清理模式切换/提示音退出标志\r
+ try:\r
+ self.Close_flag = 0\r
+ self._ptt_await_server = 0\r
+ except Exception:\r
+ pass\r
+\r
+ t0 = 0\r
+ try:\r
+ t0 = utime.ticks_ms()\r
+ except Exception:\r
+ t0 = 0\r
+\r
+ self._ptt_enter_interrupt_arm()\r
+ try:\r
+ self._vc_publish_led("listening", "ptt_begin/pre")\r
+ except Exception:\r
+ pass\r
+\r
+ # 本地先硬静音再 VC_init,避免 DMA/队列尾包漏播\r
+ try:\r
+ self._ptt_hard_mute_local_playback()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.VC_init()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:\r
+ self.VC_Play_End()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._ptt_hard_mute_local_playback()\r
+ except Exception:\r
+ pass\r
+\r
+ # 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错\r
+ try:\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ self.VC_Speech_End()\r
+ except Exception:\r
+ pass\r
+\r
+ # 已在等 canceled:只刷新「待开录」,勿重复 audio.create\r
+ try:\r
+ if getattr(self, "_vc_serial_canceling", 0):\r
+ self._ptt_pending_arm = 1\r
+ print("[PTT] cancel in progress, refresh pending arm")\r
+ self._ptt_end_led_suppress()\r
+ return\r
+ except Exception:\r
+ pass\r
+\r
+ # 会话未连接:先建链并发 Start,等待 started 后再自动开麦\r
+ try:\r
+ if getattr(self, "AI_client_VC_isconnect", 0) != 1:\r
+ self._ptt_force_record_on_started = 1\r
+ self.AI_VC_start()\r
+ return\r
+ except Exception:\r
+ self._ptt_force_record_on_started = 1\r
+ try:\r
+ self.AI_VC_start()\r
+ except Exception:\r
+ pass\r
+ return\r
+\r
+ # 会话还没 initialized:先发 Start,请求服务端进入 started\r
+ if getattr(self, "_vc_session_started", 0) != 1:\r
+ self._ptt_force_record_on_started = 1\r
+ try:\r
+ try:\r
+ if t0:\r
+ print("[PTT] wait server.started (ms={})".format(utime.ticks_ms() - t0))\r
+ except Exception:\r
+ pass\r
+ self.AI_client_VC_start_requset()\r
+ except Exception:\r
+ pass\r
+ return\r
+\r
+ # 服务端仍在思考/TTS:先 cancel,在 btn 队列收到 canceled 后再开录(方案甲串行)\r
+ if self._vc_ptt_need_interrupt_wait():\r
+ self._vc_begin_ptt_cancel_wait("ptt_begin")\r
+ return\r
+\r
+ self._ptt_arm_uplink(t0)\r
+\r
+ def _vc_effective_asr_text(self):\r
+ try:\r
+ t = (getattr(self, "AI_ASR_str", "") or "").strip()\r
+ if t:\r
+ return t\r
+ return (getattr(self, "_last_asr_result_text", "") or "").strip()\r
+ except Exception:\r
+ return ""\r
+\r
+ def _vc_long_dialogue_auto_uplink_ok(self, reason):\r
+ """长对话 canceled/completed 自动开麦:须有有效 ASR,且防抖/勿在 LLM 无 TTS 后抢开。"""\r
+ try:\r
+ if reason not in ("canceled", "completed", "local.play_end", "audio.played"):\r
+ return True\r
+ if getattr(self, "Close_flag", 0) == 1:\r
+ return False\r
+ if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:\r
+ pass\r
+ elif not self._vc_effective_asr_text():\r
+ try:\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason))\r
+ except Exception:\r
+ pass\r
+ return False\r
+ if reason == "completed":\r
+ if getattr(self, "_turn_has_tts_started", 0) == 0:\r
+ try:\r
+ if getattr(self, "_llm_completed_ms", 0) or getattr(self.fsm, "turn", None) == VCTurnState.Thinking:\r
+ print("[VC][uplink_ctrl] Deny START by completed cause=llm_without_tts")\r
+ return False\r
+ except Exception:\r
+ pass\r
+ try:\r
+ now = utime.ticks_ms()\r
+ last = getattr(self, "_vc_last_auto_uplink_ms", 0)\r
+ if last and utime.ticks_diff(now, last) < 1200:\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=auto_uplink_debounce".format(reason))\r
+ return False\r
+ self._vc_last_auto_uplink_ms = now\r
+ except Exception:\r
+ pass\r
+ except Exception:\r
+ pass\r
+ return True\r
+\r
+ def _vc_led_state_debug_enabled(self):\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ return vc_config.get_bool("LED_STATE_DEBUG", True)\r
+ except Exception:\r
+ pass\r
+ return True\r
+\r
+ def _vc_led_should_standby_idle(self):\r
+ """未录音且非 TTS 播中:待机/提示音/模式切换/单对话轮间应灭灯(DES-L3 idle),勿被 FSM.turn=Listening 误导。"""\r
+ try:\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ return False\r
+ if self._vc_tts_playback_active():\r
+ return False\r
+ if getattr(self, "_ptt_await_server", 0) == 1:\r
+ return False\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ if t in (\r
+ VCTurnState.Thinking,\r
+ VCTurnState.TtsReceiving,\r
+ VCTurnState.Playing,\r
+ VCTurnState.AwaitPlayedAck,\r
+ ):\r
+ return False\r
+ if getattr(self, "Close_flag", 0) == 1:\r
+ return True\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ return True\r
+ k = getattr(self, "_last_tts_kind", "") or ""\r
+ if k in ("prompt", "boot_prompt", "mode_switch_long", "mode_switch_ptt"):\r
+ return True\r
+ except Exception:\r
+ pass\r
+ return False\r
+\r
+ def _vc_publish_led(self, ai_state, reason=""):\r
+ try:\r
+ if getattr(self, "_ptt_led_lock_listening", 0) == 1:\r
+ if ai_state != "listening":\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[LED] publish blocked ai={} reason={} (ptt_arm_lock)".format(\r
+ ai_state, reason))\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[LED] publish ai={} reason={} VC_mode={} rec={} turn={}".format(\r
+ ai_state, reason, getattr(self, "VC_mode", -1),\r
+ getattr(self, "VC_record_state_flag", -1),\r
+ getattr(getattr(self, "fsm", None), "turn", "?")))\r
+ except Exception:\r
+ pass\r
+ try:\r
+ EventMesh.publish("set_led_state", {"ai_state": ai_state})\r
+ except Exception:\r
+ pass\r
+\r
+ def _vc_led_sync_from_fsm(self):\r
+ """按 FSM/真实 TTS 播放恢复 AI 灯;本地 aud.play 短音不参与。"""\r
+ try:\r
+ if getattr(self, "_ptt_led_lock_listening", 0) == 1:\r
+ if getattr(self, "VC_record_state_flag", 0) == 0:\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._vc_led_should_standby_idle():\r
+ self._vc_publish_led("idle", "fsm_sync/standby_idle")\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:\r
+ self._vc_publish_led("thinking", "fsm_sync/ptt_await")\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._vc_tts_playback_active():\r
+ self._vc_publish_led("playing", "fsm_sync/tts")\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ if t in (VCTurnState.Thinking, VCTurnState.TtsReceiving):\r
+ self._vc_publish_led("thinking", "fsm_sync/thinking")\r
+ elif t in (VCTurnState.Playing, VCTurnState.AwaitPlayedAck):\r
+ # 服务端已标记播放态但本地 PCM 尚未起播:仍显示思考,勿误绿灯\r
+ self._vc_publish_led("thinking", "fsm_sync/await_tts")\r
+ elif t in (VCTurnState.Listening, VCTurnState.SpeechEnded, VCTurnState.SpeechStreaming):\r
+ self._vc_publish_led("listening", "fsm_sync/listening")\r
+ else:\r
+ self._vc_publish_led("idle", "fsm_sync/idle")\r
+ except Exception:\r
+ try:\r
+ self._vc_publish_led("idle", "fsm_sync/fallback")\r
+ except Exception:\r
+ pass\r
+\r
+ def _uplink_request_start(self, reason="", *args, **kwargs):\r
+ # 统一入口:避免多处同时 start 造成冲突(尤其是 TTS/播放阶段误开麦导致回声触发与状态紊乱)\r
+ try:\r
+ if self.Close_flag == 1:\r
+ try:\r
+ if reason in ("canceled", "local.play_end", "audio.played", "key.press/continue"):\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=Close_flag".format(reason))\r
+ except Exception:\r
+ pass\r
+ return\r
+ except Exception:\r
+ return\r
+\r
+ # 连续对话才允许“自动开新一轮上行”\r
+ try:\r
+ if self.VC_mode != 0:\r
+ try:\r
+ if reason in ("canceled", "local.play_end", "audio.played"):\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=VC_mode({})".format(reason, self.VC_mode))\r
+ except Exception:\r
+ pass\r
+ return\r
+ except Exception:\r
+ return\r
+\r
+ try:\r
+ if self.VC_mode == 0 and not self._vc_long_dialogue_auto_uplink_ok(reason):\r
+ return\r
+ except Exception:\r
+ pass\r
+\r
+ try:\r
+ if not self._vc_capture_allowed(reason):\r
+ try:\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=playback_busy".format(reason))\r
+ except Exception:\r
+ pass\r
+ return\r
+ except Exception:\r
+ return\r
+\r
+ # ===== 业务门控(收敛到 FSM turn/session)=====\r
+ try:\r
+ if getattr(self.fsm, "session", VCSessionState.Disconnected) != VCSessionState.Started:\r
+ try:\r
+ if reason in ("canceled", "local.play_end", "audio.played"):\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=fsm.session".format(reason))\r
+ except Exception:\r
+ pass\r
+ return\r
+ turn = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ # 仅 Listening/SpeechEnded 允许开麦进入下一轮;其它阶段一律拒绝(避免抢跑打乱链路)\r
+ if turn not in (VCTurnState.Listening, VCTurnState.SpeechEnded):\r
+ try:\r
+ if reason in ("canceled", "local.play_end", "audio.played"):\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn))\r
+ except Exception:\r
+ pass\r
+ return\r
+ except Exception:\r
+ return\r
+\r
+ # 若当前不在录音上行,才允许开新一轮\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ # canceled 打断后:可能是僵死 flag=2(started 未真开麦),先停录复位再开麦\r
+ if reason == "canceled":\r
+ try:\r
+ self._vc_clear_stale_record_state("uplink_start/canceled")\r
+ except Exception:\r
+ pass\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ try:\r
+ if reason in ("canceled", "local.play_end", "audio.played"):\r
+ print("[VC][uplink_ctrl] Deny START by {} cause=VC_record_state_flag({})".format(\r
+ reason, getattr(self, "VC_record_state_flag", 0)))\r
+ except Exception:\r
+ pass\r
+ return\r
+\r
+ # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)\r
+ if getattr(self, "record_data_save_flag", 0) == 0:\r
+ self._server_vad_silence = 0\r
+\r
+ print("[VC][uplink_ctrl] Request START by {}".format(reason))\r
+ try:\r
+ if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:\r
+ self._vc_pending_interrupt_uplink = 0\r
+ print("[VC][uplink_ctrl] interrupt_uplink armed -> start (canceled)")\r
+ except Exception:\r
+ pass\r
+ self.VC_Speech_Start_now()\r
+\r
+ def _uplink_request_stop(self, reason="", *args, **kwargs):\r
+ # 统一入口:避免多处同时 stop 造成冲突\r
+ if getattr(self, "record_data_save_flag", 0) == 0:\r
+ self._server_vad_silence = 1\r
+\r
+ if getattr(self, "VC_record_state_flag", 0) == 0:\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] _uplink_request_stop skip (record_state==0) reason={}".format(reason))\r
+ except Exception:\r
+ pass\r
+ return\r
+\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] step=uplink_stop_exec reason={} VC_record_state_flag={}".format(\r
+ reason, getattr(self, "VC_record_state_flag", -1)))\r
+ except Exception:\r
+ pass\r
+ print("[VC][uplink_ctrl] Request STOP by {}".format(reason))\r
+ self.VC_Speech_End()\r
+\r
+ def _uplink_mark_start(self, first_len=0):\r
+ try:\r
+ if self._uplink_active == 1:\r
+ return\r
+ self._uplink_active = 1\r
+ self._uplink_bytes = 0\r
+ self._uplink_start_ms = utime.ticks_ms()\r
+ print("[VC][uplink] START len={} {} VC_record_state_flag={}".format(\r
+ first_len, self._vc_fsm_log_snip(), self.VC_record_state_flag))\r
+ except Exception:\r
+ pass\r
+\r
+ def _uplink_mark_end(self, reason=""):\r
+ try:\r
+ if self._uplink_active != 1:\r
+ return\r
+ dur = utime.ticks_diff(utime.ticks_ms(), self._uplink_start_ms) if self._uplink_start_ms else -1\r
+ print("[VC][uplink] END reason={} bytes={} dur_ms={} {} VC_record_state_flag={}".format(\r
+ reason, self._uplink_bytes, dur, self._vc_fsm_log_snip(), self.VC_record_state_flag))\r
+ except Exception:\r
+ pass\r
+ self._uplink_active = 0\r
+ self._uplink_bytes = 0\r
+ self._uplink_start_ms = 0\r
+\r
+ # 最近一次 TTS 的语义来源(dialog/prompt/mode_switch_long/mode_switch_ptt/...)\r
+ self._last_tts_kind = ""\r
+ # 下一个“无 dialog_id 的提示音 TTS”在 tts.started 时采用该 kind\r
+ self._pending_tts_kind = ""\r
+\r
+ # ===== 状态机动作接口(骨架)=====\r
+ # 说明:统一把“副作用动作”收敛到这些方法,便于后续把 recv/timer 的 if/flag 逻辑迁移到 VCStateMachine。\r
+ def request_send_start(self):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_start_requset})\r
+\r
+ def request_send_audio_create(self):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_send_speech})\r
+\r
+ def request_send_audio_completed(self):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_end_speech})\r
+\r
+ def request_send_audio_played(self):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_audio_played})\r
+\r
+ def request_send_synthesis(self, text):\r
+ try:\r
+ self.AI_TTS_str = text\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})\r
+\r
+ def request_uplink_start(self, reason=""):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_start, "data": reason})\r
+\r
+ def request_uplink_stop(self, reason=""):\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_stop, "data": reason})\r
+\r
+ def _fsm_apply_actions(self, actions):\r
+ try:\r
+ for a in actions:\r
+ if not a:\r
+ continue\r
+ if a[0] == "uplink_stop":\r
+ self.request_uplink_stop(a[1] if len(a) > 1 else "")\r
+ elif a[0] == "uplink_start":\r
+ self.request_uplink_start(a[1] if len(a) > 1 else "")\r
+ elif a[0] == "play_start":\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ if self._ptt_should_skip_remote_playback():\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[VC][serial] skip play_start (ptt_interrupt)")\r
+ continue\r
+ try:\r
+ self.fsm.on_local_play_start()\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_Start})\r
+ elif a[0] == "send_audio_played":\r
+ self.request_send_audio_played()\r
+ except Exception:\r
+ pass\r
+\r
+ # AI\r
+\r
+ def _print_hw_diag(self):\r
+ # 仅诊断使用:打印当前分支能看到的麦克/音频相关配置和运行时状态。\r
+ # JSWM2 老分支没有 device_profile/_PROFILE,这里宽容处理\r
+ try:\r
+ profile = _PROFILE if isinstance(_PROFILE, dict) else {}\r
+ except Exception:\r
+ profile = {}\r
+ dev_type_profile = profile.get("dev_type", None)\r
+ flags = profile.get("flags", {})\r
+ hardware = profile.get("hardware", {})\r
+ product_cfg = profile.get("product_cfg", {})\r
+ vc_cfg = {}\r
+ try:\r
+ vc_cfg = product_cfg.get("vc", {})\r
+ except Exception:\r
+ vc_cfg = {}\r
+\r
+ print("[hw-diag] ===== AI_VC hardware diag =====")\r
+ print("[hw-diag] profile.dev_type =", dev_type_profile)\r
+ print("[hw-diag] profile.flags =", flags)\r
+ print("[hw-diag] profile.hardware =", hardware)\r
+ print("[hw-diag] product_cfg.vc =", vc_cfg)\r
+\r
+ try:\r
+ print("[hw-diag] VC_record_samplerate =", self.VC_record_samplerate)\r
+ print("[hw-diag] VC_play_samplerate =", self.VC_play_samplerate)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ print("[hw-diag] vad_silence_runtime =", self.vad_silence)\r
+ except Exception:\r
+ pass\r
+ try:\r
+ print("[hw-diag] record_data_save_flag =", self.record_data_save_flag)\r
+ print("[hw-diag] record_max_bytes =", self.record_max_bytes)\r
+ except Exception:\r
+ pass\r
+\r
+ try:\r
+ g = self.record.gain_get()\r
+ except Exception as e:\r
+ g = "gain_get_error: {}".format(e)\r
+ print("[hw-diag] record.gain_get =", g)\r
+ try:\r
+ print("[hw-diag] type(record) =", type(self.record))\r
+ print("[hw-diag] type(aud) =", type(self.aud))\r
+ except Exception:\r
+ pass\r
+ print("[hw-diag] ===== end of hw-diag =====")\r
+\r
+ def __init__(self):\r
+ # import voiceCall\r
+ # voiceCall.callStart("10086")\r
+ # return\r
+ self.http_get_config_flag = 0\r
+ if self.http_get_config_flag == 1:\r
+ try:\r
+ self.http_get_config()#获取/更新配置参数\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.VC_30S_timer = osTimer()\r
+ self.ai_key_press_audio = osTimer()\r
+ self.VC_30S_timer.start(30000, 1, self.VC_30S_timer_cb)\r
+ self.online_kws = 0#是否开启线上关键词\r
+ self.get_config_timer_count = 0\r
+ self.record_data_save_flag = 0 #是否保存录音文件\r
+ self.play_data_save_flag = 0#是否保存tts数据到文件\r
+ self.record_fd = None\r
+ self.play_fd = None\r
+ self.is_VC_ACK = 0 #是否开启ack应答模式\r
+ self.is_long_record = 0 #是否开启长录音模式(语音打断模式)\r
+ self.ai_OTA = OTA()\r
+ self.is_auth = 1 #是否开启dmp鉴权\r
+ #self.VC_mode = 1 #0:短按连续问答模式 1:长按录音模式\r
+ self.VC_mode = EventMesh.publish("get_storage_config","VC_mode")#\r
+ if self.VC_mode == None :\r
+ self.VC_mode = 0\r
+ self.is_up_error= 0 #是否上传错误id\r
+ self.Close_flag =0 #=1时,连续对话播放完音频就退出\r
+ # 服务端 VAD 结束标志:收到 vad.silence 后置 1,用于硬闸门禁止继续上行音频\r
+ # (避免仍持续上行导致服务端推迟/抑制后续 TTS,从而一直停留在思考态蓝绿闪)\r
+ self._server_vad_silence = 0\r
+ # 单轮状态标志:用于判定 completed 是否应自动进入下一轮\r
+ self._turn_has_asr_completed = 0\r
+ self._turn_has_vad_silence = 0\r
+ self._turn_has_tts_started = 0\r
+ # VC 会话是否已进入 started(服务端 session initialized)\r
+ # 重要:在收到 started 前发送音频帧会触发服务端 "session not initialized" → failed → 端侧重连死循环\r
+ self._vc_session_started = 0\r
+ # 开机/提示音:本轮 started 不自动开麦(避免问候 TTS 前误进 listening)\r
+ self._skip_started_uplink_once = 0\r
+ self._last_tts_kind = ""\r
+ self._pending_tts_kind = ""\r
+ # Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create\r
+ # 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed\r
+ self._ptt_audio_created = 0\r
+ # PTT:松手等服务器期间保持 thinking 勿回蓝闪聆听\r
+ self._ptt_await_server = 0\r
+ # 方案甲 btnInfo 串行:打断时先 cancel,收到 canceled(或超时)后再 audio.create\r
+ self._vc_serial_canceling = 0\r
+ self._ptt_pending_arm = 0\r
+ self._vc_cancel_deadline_ms = 0\r
+ # PTT 打断:轮次号 + 开录前丢弃旧 TTS / 灯只显示聆听\r
+ self._ptt_arm_epoch = 0\r
+ self._ptt_interrupt_active = 0\r
+ self._ptt_led_lock_listening = 0\r
+ # 端侧主动发 synthesis 后到收到 tts.started 之间的窗口标志,用于阻止误开下一轮录音上行\r
+ self._tts_pending = 0\r
+ self._tts_pending_ms = 0\r
+ # 各类兜底的节流时间戳(避免“兜底过激/连续触发”)\r
+ self._fb_last_thinking_restart_ms = 0\r
+ self._fb_last_tts_pending_timeout_ms = 0\r
+ # thinking 兜底:部分情况下服务端只返回 llm.completed 而不触发 tts.started/completed,\r
+ # 会导致一直停留在 thinking。此时超时自动回到聆听。\r
+ self._llm_completed_ms = 0\r
+ # 语音上行统计(真实发送到 VC 发送队列的开始/结束)\r
+ self._uplink_active = 0\r
+ self._uplink_bytes = 0\r
+ self._uplink_start_ms = 0\r
+\r
+ # 状态机骨架(后续逐步把旧逻辑迁移进来)\r
+ self.fsm = VCStateMachine(self)\r
+\r
+ self.play_new_voice_flag =0 #播放文字留言flag\r
+\r
+ self.wait_first_flag = 0#是否进入未读判定(只播报一次)\r
+ self.wait_first_timer = osTimer()#如果播放未读留言的时候未读则不要播放了\r
+ self.wait_first_cnt = 0\r
+\r
+ self.wait_sort = 0#未读留言正在播第几条\r
+ EventMesh.subscribe("play_tts",self.play_tts)\r
+ EventMesh.subscribe("play_new_voice",self.play_new_voice)\r
+ EventMesh.subscribe("play_his_voice",self.play_his_voice)\r
+ EventMesh.subscribe("play_wait_voice",self.play_wait_voice)\r
+ EventMesh.subscribe("play_single_voice",self.play_single_voice)#不带tts的播放(闹钟)\r
+ EventMesh.subscribe("play_music",self.play_music)#播放音乐\r
+ EventMesh.subscribe("play_trans_tts",self.play_trans_tts)#接收翻译tts\r
+ EventMesh.subscribe("set_vad_silence",self.set_vad_silence)\r
+ EventMesh.subscribe("set_wait_first_flag",self.set_wait_first_flag)\r
+ EventMesh.subscribe("kws_even",self.kws_even)\r
+ EventMesh.subscribe("change_volume",self.change_volume) \r
+ EventMesh.subscribe("find_cmd",self.find_cmd) #查询命令\r
+ EventMesh.subscribe("NLP_set_vad_silence",self.NLP_set_vad_silence) #设置聆听间隔\r
+ EventMesh.subscribe("change_voice",self.change_voice) #改变音色\r
+ EventMesh.subscribe("Close_VC",self.Close_VC) #外部结束对话\r
+ EventMesh.subscribe("get_recv_voice",self.get_recv_voice) #获取留言播放状态\r
+ EventMesh.subscribe("set_recv_voice",self.set_recv_voice) #设置留言播放状态\r
+ self.voice_timer = osTimer()#语音留言10S按下按键确定是否播放的判断定时器\r
+ self.voice_cnt = 0 #留言定时器计数\r
+ self.voicerupt_flag = 0#留言选择按键接听标志位\r
+ self.wait_voicerupt_flag = 0#未读留言选择按键接听标志位\r
+ self.recv_voice = 0 #是否收到留言\r
+ self.voice_notice = 0 #收到留言并播放则置为1,接下来要播放提示词\r
+ self.voice_choose_flag = 0#当tts和语音留言冲突时,为了防止语音地址被误清,做标志位在close做判断\r
+\r
+ self.revoice_timer = osTimer()#语音留言10S按下按键确定是否回复的判断定时器\r
+ self.revoice_cnt = 0 #回复留言定时器计数\r
+ self.revoicerupt_flag = 0#留言选择按键接听标志位\r
+\r
+ self.music_timer = osTimer()#音乐播放定时器,等待tts播完才放音乐\r
+ self.music_cnt = 0 #留言定时器计数\r
+\r
+ self.his_timer = osTimer()#历史留言播放定时器,等待tts播完再播留言\r
+ self.his_cnt = 0#历史留言定时器技术\r
+\r
+ self.wait_timer = osTimer()#未读留言播放定时器,等待tts播完再播留言\r
+ self.wait_cnt = 0#未读留言定时器技术\r
+\r
+ self.error_data = []\r
+ self.asr_taskid = ""\r
+ self.VC_taskid = ""\r
+ self.VC_server = 2 #0柒牌正式服务器,1:测试服务器1,2:测试服务器2\r
+ self.voice_flag = 0\r
+\r
+ #会议记录标志\r
+ self.meet_flag = 0\r
+ #AI翻译标志\r
+ self.trans_flag = 0\r
+\r
+ #历史留言查看判断标志\r
+ self.history_voice_flag = 0#公共变量判断 \r
+ #60S等待留言判断标志\r
+ self.wait_voice_flag = 0#公共变量判断 \r
+ EventMesh.publish("AI_alarm_open")#\r
+ self.server_ip = EventMesh.publish("get_storage_config","server_ip")#\r
+ if self.server_ip == None:\r
+ if self.VC_server == 1:# \r
+ self.server_ip="118.145.158.207:22083"\r
+ elif self.VC_server == 2:# \r
+ self.server_ip="118.145.158.207:22082"\r
+ elif self.VC_server == 4:# \r
+ self.server_ip="118.145.158.207:22084"\r
+ elif self.VC_server == 5:# \r
+ self.server_ip="amp-wss.raisound.com"\r
+ elif self.VC_server == 6:# \r
+ self.server_ip="118.145.158.207:22080"\r
+ elif self.VC_server == 7:# \r
+ self.server_ip="118.145.158.207:22080"\r
+ elif self.VC_server == 8:# \r
+ self.server_ip="115.190.94.208:22080"\r
+ elif self.VC_server == 86:# \r
+ self.server_ip="118.145.158.207:22086" \r
+ else:\r
+ #self.server_ip="sevenpin.qipai.com.cn"\r
+ self.server_ip="118.145.158.207:22082"\r
+ # 默认走 constants.AMP_SERVER(由 default_srv.py 统一管理,可在 usr 覆盖)\r
+ try:\r
+ if constants.AMP_SERVER:\r
+ self.server_ip = constants.AMP_SERVER\r
+ except Exception:\r
+ pass\r
+\r
+ #开机提示音\r
+ self.power_on_tone = EventMesh.publish("get_storage_config","power_on_tone")#\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ if not vc_config.get_bool("POWER_ON_TONE_ENABLED", True):\r
+ self.power_on_tone = None\r
+ except Exception:\r
+ pass\r
+ if self.power_on_tone == None:\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ self.power_on_tone = vc_config.get_attr("POWER_ON_TONE_DEFAULT", None)\r
+ except Exception:\r
+ pass\r
+\r
+ self.VC_record_TYPE = VC_record_TYPE_PCM #0:PCM 1:AMR\r
+ self.VC_play_TYPE = VC_play_TYPE_PCM #0:PCM 1:mp3 \r
+ self.VC_record_samplerate = 16000\r
+ self.VC_play_samplerate = 16000\r
+ self.VAD_data_len = 640 #10ms数据\r
+ self.is_VAD = 0#=1打开vad打断\r
+ # ARG_sample_rate, // 采样率\r
+ # ARG_channel, // 通道数\r
+ # ARG_frame_size, // 每帧字节数\r
+ # ARG_energy_threshold, // 能量阈值\r
+ # ARG_voice_time, // 语音持续时间阈值(ms)\r
+ # ARG_silence_time, // 静音持续时间阈值(ms)\r
+ # ARG_silence_start_threshold, // 静音开始阈值(ms)\r
+ self.vad = None\r
+ if self.is_VAD == 1:\r
+ self.vad = VAD.VAD(self.VC_record_samplerate ,1,self.VAD_data_len,600,100,300,100)\r
+ self.speech_rate = EventMesh.publish("get_storage_config","speech_rate")#语速配置\r
+ if self.speech_rate == None:\r
+ self.speech_rate = 0\r
+ \r
+ self.vad_silence = EventMesh.publish("get_storage_config","vad_silence")#语速配置\r
+ if self.vad_silence == None:\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ self.vad_silence = vc_config.get_int("VAD_SILENCE_DEFAULT", 300)\r
+ else:\r
+ self.vad_silence = 300\r
+ except Exception:\r
+ self.vad_silence = 300\r
+\r
+ self.VC_TTS_voice_type = EventMesh.publish("get_storage_config","VC_TTS_voice_type")#音色配置\r
+ if self.VC_TTS_voice_type == None :\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ cfg = vc_config.get_attr("VC_TTS_VOICE_TYPE_DEFAULT", None)\r
+ if isinstance(cfg, dict):\r
+ self.VC_TTS_voice_type = cfg\r
+ except Exception:\r
+ pass\r
+ # 若配置未提供,沿用原逻辑内置兜底(保持兼容)\r
+ if self.VC_TTS_voice_type == None:\r
+ self.VC_TTS_voice_type = {\r
+ "女":{"bot_name":"柒妹妹","voice":"zh_female_wanwanxiaohe_moon_bigtts","lang":"zh-cn"},\r
+ "男":{"bot_name":"柒先生","voice":"zh_male_wennuanahu_moon_bigtts","lang":"zh-cn"},\r
+ "童":{"bot_name":"柒乐乐","voice":"ICL_zh_female_huoponvhai_tob","lang":"zh-cn"},\r
+ }\r
+ \r
+ # self.VC_TTS_voice_type = {0:"zh_female_xinlingjitang_moon_bigtts",1:"zh_male_wennuanahu_moon_bigtts",2:"ICL_zh_female_huoponvhai_tob",\r
+ # 3:"aliyun::longjiaxin",4:"zh_female_daimengchuanmei_moon_bigtts"}\r
+ #self.VC_TTS_voice_type = ["aliyun::longxiaoxia","aliyun::longnan","aliyun::longling"]\r
+ \r
+ #EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])\r
+\r
+ try:\r
+ self.VC_TTS_voice_key = EventMesh.publish("get_storage_config","VC_TTS_voice_key")#音色配置\r
+ if self.VC_TTS_voice_key == None :\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ self.VC_TTS_voice_key = vc_config.get_attr("VC_TTS_VOICE_KEY_DEFAULT", None)\r
+ except Exception:\r
+ pass\r
+ if not self.VC_TTS_voice_key:\r
+ self.VC_TTS_voice_key = "男"\r
+ if self.VC_TTS_voice_key in self.VC_TTS_voice_type:\r
+ pass\r
+ else:\r
+ self.VC_TTS_voice_key = list(self.VC_TTS_voice_type.keys())[0]\r
+ print("音色配置:{}:{}\r\n\r\n".format(self.VC_TTS_voice_key,self.VC_TTS_voice_type[self.VC_TTS_voice_key]))\r
+ self.VC_TTS_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ #配置词\r
+ self.commands_mode = 2 #=1用大模型判断命令,=0用服务器本地插件判断命令 =2用设备本地判断方式\r
+ try:\r
+ self._vc_intent_selftest_once()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.commands = []\r
+ if self.VC_TTS_volume == None:\r
+ self.VC_TTS_volume = 100\r
+ #可ota配置的参数变量\r
+ self.emotion = EventMesh.publish("get_storage_config","emotion")#情绪\r
+ if self.emotion == None:\r
+ self.emotion = ""\r
+ self.ota_update_notice = EventMesh.publish("get_storage_config","ota_update_notice")#OTA升级提示语\r
+ if self.ota_update_notice == None:\r
+ self.ota_update_notice = "" \r
+ self.AI_audio_queue_thread_id = 0\r
+ self.AI_audio_queue_thread_lock = 0\r
+ self.AI_VC_send_data_queue_thread_id = 0\r
+ self.AI_VC_send_data_queue_thread_lock = 0\r
+ self.AI_ASR_send_data_queue_thread_id = 0\r
+ self.AI_ASR_send_data_queue_thread_lock = 0\r
+ self.AI_record_tmp_queue = Queue(100)\r
+ self.AI_ASR_send_data_queue = Queue(200)\r
+ self.AI_VC_send_data_queue = Queue(100)\r
+ self.AI_audio_queue = Queue(100)\r
+ #self.AI_WS_connect_Queue = Queue(100) #释放用\r
+ self.AI_TTS_str = ""\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_flag = 0\r
+ self.AI_ASR_str_tmp = ""\r
+ self.AI_ASR_send_buf_len = 0\r
+ self.AI_ASR_text_tmp = ""\r
+ self.AI_ASR_text_repet_count = 0\r
+ # #开始线程\r
+ # if self.AI_audio_queue_thread_id ==0:\r
+ # self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())\r
+ # else:\r
+ # print("AI_VC_start :AI_audio_queue_thread_id != 0 ")\r
+ # if self.AI_VC_send_data_queue_thread_id == 0: \r
+ # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())\r
+ \r
+ self.VC_dialog_id = ""#\r
+ self.VC_message_id=None\r
+ self.VC_task_id =None\r
+ self.AI_client_VC = None\r
+ self.AI_client_VC_recv_id = 0\r
+ self.AI_client_VC_recv_lock = 0\r
+ self.AI_client_VC_isconnect =0\r
+ self.AI_client_VC_debug =0\r
+\r
+ self.asr_state = 0 # 预留:ASR 侧状态(当前未参与分支)\r
+ self.key_state = 0\r
+\r
+ self.AI_client_asr = None\r
+ self.AI_client_asr_recv_id = 0\r
+ self.AI_client_asr_recv_lock = 0\r
+ self.AI_client_asr_isconnect =0\r
+ self.AI_ASR_timer = osTimer()\r
+ self.AI_ASR_timer_count = 0\r
+ self.AI_client_asr_debug =0\r
+ self.AI_ASR_check_timeout = 0\r
+ self.AI_ASR_error_timeout = 0\r
+ self.VC_Thinking_timerout = 0\r
+\r
+\r
+ self.record_pcm = None\r
+ self.play_pcm = None\r
+ self.VC_record_thread_id = 0\r
+ self.VC_record_thread_lock = 0\r
+ self.VC_pingping_timerout = 0\r
+ self.asr_pingping_timerout = 0\r
+ self.factory_timer = osTimer()\r
+ self._1S_timer = osTimer()\r
+ self.Audio_buf_size = 20*1024\r
+ self.record = audio.Record()\r
+ #self.record.end_callback(self.record_callback)\r
+ self.record.gain_set(4,10)\r
+ #self.record.gain_set(4,12)\r
+ self.aud = audio.Audio(0)\r
+ self.aud.setCallback(self.audio_cb)\r
+ # 确保 record/aud 已创建后再打印硬件/音频诊断信息(需要 gain_get/type(record))\r
+ try:\r
+ self._print_hw_diag()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ #self.aud.setVolume(self.VC_TTS_volume //10)\r
+ self.audio_play_state = 1 #音频播放完状态\r
+ self.close_timerout = 0\r
+ self._idle_deep_sec = 0\r
+ self._vc_idle_deep_armed_suppress = 0\r
+ self.LocalRespondingEnded_timerout = 0\r
+\r
+ self.VC_Play_flag = 0\r
+ self.play_flag = 0\r
+ self.VC_record_state_flag = 0\r
+ self.AI_key_long_press_flag = 0\r
+ self._asr_completed_tone_done = 0 # asr.completed 本地 think 提示每轮仅一次\r
+ self._last_asr_result_text = "" # 保留最近 asr.result,防 VC_Speech_End 早于 asr.completed 清空\r
+ self._vc_last_auto_uplink_ms = 0 # 长对话自动开麦防抖\r
+ self._vc_local_play_active = 0 # aud.play 本地短音占用(与采集互斥)\r
+ self._vc_local_play_until_ms = 0\r
+ self._vc_pending_interrupt_uplink = 0 # 长对话短按打断:等 canceled 再开麦\r
+ self.websocket_error_count = 0\r
+ self.AI_new_voice_url = "" #未读语音地址/音乐播放地址/闹钟播放标志\r
+ self.music_play_flag = 0 #音乐播放标志\r
+ self.alarm_play_flag = 0 #闹钟播放标志\r
+ self.tts_in_data_len = 0\r
+ self.tts_out_data_len = 0\r
+ self._need_audio_played_ack = 0#tts.completed 后需在本地播报结束后上报 audio.played;勿用 tts_in/out:tts.completed 会再调 VC_Play_Start 把二者清零\r
+ self.VC_satrt_flag = 0\r
+ self.VC_stop_play_audio_flag = 0\r
+ self.VC_satrt_kws_flag = 0\r
+ self.session_id = ""\r
+ self.stop_voice_timer=osTimer()\r
+ self.pwk_cnt_voice = 0 #不听语音后,积攒的次数,需要一起播放\r
+ self.pwk_notice_flag = 0#是否处于积攒留言状态(用来实现单次提醒)\r
+ self.is_noread_voice = EventMesh.publish("get_storage_config","is_noread_voice")\r
+ if self.is_noread_voice == None:\r
+ self.is_noread_voice = 0#是否还有未读留言(开机判断)\r
+ self.is_ruppt_tts = 0#是否已经打断tts播放\r
+ EventMesh.subscribe("AI_key_press", self.AI_key_press)\r
+ EventMesh.subscribe("BT_AI_key_press", self.BT_AI_key_press)\r
+ EventMesh.subscribe("stop_AI_record", self.stop_AI_record)\r
+ EventMesh.subscribe("AI_key_release", self.AI_key_release)\r
+ EventMesh.subscribe("pwk_key_double_press", self.pwk_key_double_press)\r
+ EventMesh.subscribe("AI_key_long_press", self.AI_key_long_press)\r
+ EventMesh.subscribe("pwk_key_release", self.pwk_key_release)\r
+ EventMesh.subscribe("get_AI_audio_queue", self.get_AI_audio_queue)\r
+ EventMesh.subscribe("get_VC_satrt_flag", self.get_VC_satrt_flag)\r
+ EventMesh.subscribe("get_vc_stream_active", self.get_vc_stream_active)\r
+ EventMesh.subscribe("get_vc_tts_playback_active", self.get_vc_tts_playback_active)\r
+ EventMesh.subscribe("battery_priority_mute_vc", self.battery_priority_mute_vc)\r
+ EventMesh.subscribe("VC_error", self.VC_error)\r
+ EventMesh.subscribe("audio_test", self.audio_test)\r
+ EventMesh.subscribe("audio_test_close", self.audio_test_close)\r
+ EventMesh.subscribe("set_record_data_save_flag", self.set_record_data_save_flag)\r
+ EventMesh.subscribe("set_VC_TTS_voice_type", self.set_VC_TTS_voice_type)\r
+ EventMesh.subscribe("SET_TTS_VOLUME", self.set_tts_volume)\r
+ EventMesh.subscribe("set_VC_stop_play_audio_flag", self.set_VC_stop_play_audio_flag)\r
+ EventMesh.subscribe("set_yuyinaudio_flag",self.set_yuyinaudio_flag)\r
+ EventMesh.subscribe("get_yuyinaudio_flag",self.get_yuyinaudio_flag)\r
+ EventMesh.subscribe("set_pwk_cnt_voice",self.set_pwk_cnt_voice)\r
+ EventMesh.subscribe("get_pwk_cnt_voice",self.get_pwk_cnt_voice)\r
+ EventMesh.subscribe("exit_voice_message_mode",self.exit_voice_message_mode) #超时未退出语音留言模式\r
+ EventMesh.subscribe("come_meet_mode",self.come_meet_mode)#进入会议模式\r
+ EventMesh.subscribe("exit_meet_mode",self.exit_meet_mode)#退出会议模式\r
+ EventMesh.subscribe("come_trans_mode",self.come_trans_mode)#进入翻译模式\r
+ EventMesh.subscribe("exit_trans_mode",self.exit_trans_mode)#退出翻译模式\r
+ EventMesh.subscribe("set_trans_flag",self.set_trans_flag)#设置翻译状态\r
+ EventMesh.subscribe("get_audio_play_state",self.get_audio_play_state)\r
+ EventMesh.subscribe("set_voicerupt_flag",self.set_voicerupt_flag)#按键中断触发留言播放\r
+ EventMesh.subscribe("set_wait_voicerupt_flag",self.set_wait_voicerupt_flag)#按键中断触发未读留言播放 \r
+ EventMesh.subscribe("get_recv_voice",self.get_recv_voice)#获取是否收到留言\r
+ EventMesh.subscribe("rate_change",self.rate_change)#直接修改语速\r
+ EventMesh.subscribe("get_history_voice_flag",self.get_history_voice_flag)#获取查找历史留言状态\r
+ EventMesh.subscribe("set_history_voice_flag",self.set_history_voice_flag)#修改查找历史留言状态\r
+ EventMesh.subscribe("clean_unread_vocie",self.clean_unread_vocie)#修改查找历史留言状态\r
+ EventMesh.subscribe("get_wait_voice_flag",self.get_wait_voice_flag)#获取60S等待留言播放定时器状态\r
+ EventMesh.subscribe("set_wait_voice_flag",self.set_wait_voice_flag)#修改60S等待留言播放定时器状态\r
+ EventMesh.subscribe("rate_move",self.rate_move)#NLP修改语速\r
+ EventMesh.subscribe("find_rate",self.find_rate)#NLP查询语速\r
+ EventMesh.subscribe("set_pwk_notice_flag",self.set_pwk_notice_flag)#设置未读状态\r
+ # EventMesh.subscribe("get_voicerupt_flag",self.get_voicerupt_flag)#获取留言播放状态\r
+ EventMesh.subscribe("chenggong_tone",self.chenggong_tone)\r
+ \r
+ pass\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
+ self.dialog_attributes_provider = EventMesh.publish("get_storage_config","dialog_attributes_provider")#智能体类型配置\r
+ if self.dialog_attributes_provider == None:\r
+ self.dialog_attributes_provider = "coze"\r
+ self.outbound_attributess_provider = EventMesh.publish("get_storage_config","outbound_attributess_provider")#tts服务选择\r
+ if self.outbound_attributess_provider == None:\r
+ self.outbound_attributess_provider = "volcengine"\r
+ self.inbound_attributess_provider = EventMesh.publish("get_storage_config","inbound_attributess_provider")#tts服务选择\r
+ if self.inbound_attributess_provider == None:\r
+ self.inbound_attributess_provider = "volcengine"\r
+ \r
+ self.bot_name = "柒先生"\r
+ try:\r
+ if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
+ self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]\r
+ else:\r
+ print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try:\r
+ bt_ota_flag=0\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("BT_OTA_ENABLED", False):\r
+ bt_ota_flag = self.ai_OTA.bt_ota()#先检查蓝牙是否要升级\r
+ except Exception:\r
+ pass\r
+\r
+ cat1_ota_flag = self.ai_OTA.min_sys_ota()\r
+ if bt_ota_flag==1 or cat1_ota_flag == 1:\r
+ #if 0: \r
+ if self.ota_update_notice != "":\r
+ self.AI_TTS_str = self.ota_update_notice\r
+ else:\r
+ self.AI_TTS_str = "检测到有新版本需要升级啦!升级时可千万别断电或乱按哦,升级完成后设备会自动重启,重启就代表升级成功"\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ self.Close_flag = 1\r
+ # if self.ai_OTA.min_sys_ota()==2:\r
+ # self.AI_TTS_str = "检测到有新版本需要升级啦!电量低于40%,无法保证升级正常执行,请充满电后重启设备再次升级!"\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ else:\r
+ try:\r
+ self.ai_OTA.ota_result()\r
+ self.ai_OTA.pots_ICCID()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ #开机提示语,如果联网失败,直接提示信号弱\r
+ \r
+ \r
+ #EventMesh.publish("get_storage_config",["powerRestart",1])\r
+ powerRestart = EventMesh.publish("get_storage_config","powerRestart")#=1非正常重启\r
+ if powerRestart == None:\r
+ powerRestart = 0\r
+ if powerRestart == 1:\r
+ EventMesh.publish("set_storage_config",["powerRestart",0])\r
+ \r
+ else:\r
+ pass\r
+ self.AI_TTS_str = self.power_on_tone.replace("bot_name", self.bot_name)\r
+ print("开机提示:{}".format(self.AI_TTS_str))\r
+ try:\r
+ self._pending_tts_kind = "boot_prompt"\r
+ self._last_tts_kind = "boot_prompt"\r
+ self._skip_started_uplink_once = 1\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ def set_VC_stop_play_audio_flag(self,event = None, msg = None):\r
+ self.VC_stop_play_audio_flag = msg\r
+ print("self.VC_stop_play_audio_flag:{}".format(self.VC_stop_play_audio_flag))\r
+\r
+ def rate_change(self,event,msg):#直接设置语速(数字)\r
+ self.speech_rate = msg\r
+ EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])\r
+ print("set_speech_rate:{}".format(self.speech_rate))\r
+ def rate_move(self,event,msg):#设置语速(模糊(快/慢一点))\r
+ if msg == 1 and self.speech_rate<50:#增大\r
+ self.speech_rate += 10\r
+ else:#减小\r
+ if self.speech_rate >-50:\r
+ self.speech_rate -= 10\r
+ EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])\r
+ print("set_speech_rate:{}".format(self.speech_rate))\r
+ def find_rate(self,event,msg):#查询APP语速\r
+ print("查询语速处理:原始数据为:{}".format(self.speech_rate))\r
+ y = 1\r
+ if self.speech_rate >= -50 and self.speech_rate < 0:#负数\r
+ y = 1 - (-0.01 * self.speech_rate) \r
+ elif self.speech_rate <= 50 and self.speech_rate >= 0:#正数\r
+ y = 0.02 * self.speech_rate + 1.0\r
+ print("处理后数据为{}".format(y))\r
+ rate = str(y) + "倍"\r
+ return rate\r
+ def set_pwk_notice_flag(self,event,msg):#设置未读状态\r
+ self.pwk_notice_flag = msg\r
+ #获取60S等待留言播放定时器状态\r
+ def get_wait_voice_flag(self,event,msg):\r
+ return self.wait_voice_flag\r
+ def set_wait_voice_flag(self,event,msg):\r
+ # print("未读留言标记修改为:{}".format(msg))\r
+ self.wait_voice_flag = msg\r
+\r
+ #查看历史留言状态修改\r
+ def get_history_voice_flag(self,event,msg):\r
+ return self.history_voice_flag\r
+ def set_history_voice_flag(self,event,msg):\r
+ # print("历史留言标记修改为:{}".format(msg))\r
+ self.history_voice_flag = msg\r
+ def clean_unread_vocie(self,event,msg):\r
+ self.pwk_cnt_voice = 0\r
+ self.pwk_notice_flag = 0\r
+ EventMesh.publish("set_app_new_voice_flag",0)\r
+ def audio_test(self,event = None, msg = None):\r
+ self._uplink_request_start("audio_test")\r
+ self.VC_Play_Start() \r
+ def audio_test_close(self,event = None, msg = None):\r
+ self._uplink_request_stop("audio_test_close")\r
+ self.VC_Play_End() \r
+ if self.record_data_save_flag == 1:\r
+ if self.record_fd :\r
+ self.record_fd.close()\r
+ self.record_fd = None\r
+ def set_record_data_save_flag(self,event = None, msg = None):\r
+ self.record_data_save_flag = msg\r
+ def post_processor_after_instantiation(self):\r
+ pass\r
+ def get_AI_audio_queue(self, event, msg):\r
+ return self.AI_audio_queue\r
+ def get_VC_satrt_flag(self, event, msg):\r
+ return self.VC_satrt_flag\r
+ #检查有没有意外打开ai部分功能\r
+ def check_vc_start(self):\r
+ print("check_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))\r
+ if self.VC_satrt_flag == 0:\r
+ if self.VC_record_state_flag != 0 or self.AI_client_VC_isconnect == 1 or self.VC_Play_flag == 1:\r
+ self.Close()\r
+ def VC_30S_timer_cb(self, *args): #\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_30S_timer_cb_}) \r
+\r
+ def VC_30S_timer_cb_(self): \r
+ self.get_config_timer_count += 1\r
+ if self.get_config_timer_count >= 2*60*2:#2小时更新配置参数\r
+ self.get_config_timer_count = 0\r
+ if self.http_get_config_flag == 1:\r
+ try:\r
+ self.http_get_config()#获取/更新配置参数\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.check_vc_start()#30秒检查一下有没有关闭\r
+ #提交错误id \r
+ def ud_error(self,error_data):\r
+ response = None\r
+ if self.is_up_error == 1:\r
+ try:\r
+ # currentVersion = versions.Build_Time\r
+ # currentVersion = "7PIN-"+currentVersion[:10]+"-"+currentVersion[11:-3]\r
+ url = "http://{}/health?task_id={}".format(self.server_ip,error_data[0]+error_data[1])\r
+ print(url)\r
+ response = request.get(url,timeout=5)\r
+ if response.status_code == 200:\r
+\r
+ print("ud_error:ok")\r
+ else:\r
+ print("ud_error error!!!!")\r
+ for i in response.text:\r
+ print(i)\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+ #获取配置参数\r
+ def http_get_config(self, event=None, msg=None):\r
+ print("http_get_config")\r
+\r
+ response = None\r
+ #http://180.184.206.142/bz-api/dev/config?deviceId=xxx\r
+ try:\r
+ url = (constants.DMP_BASE + "/dev/config/{}").format(modem.getDevImei())\r
+ \r
+ \r
+ print(url)\r
+ response = request.get(url,timeout=5)\r
+ if response.status_code == 200:\r
+ data = response.json()\r
+ print("http_get_config:{}".format(data))\r
+ \r
+ if data:\r
+ try:\r
+ if data["code"] == 20000:#有配置\r
+ if data["data"] :\r
+ if "version" in data["data"]:\r
+ config_version = EventMesh.publish("get_storage_config","config_version")#音量配置\r
+ if config_version != data["data"]["version"]:\r
+ config_version = data["data"]["version"]\r
+ EventMesh.publish("set_storage_config",["config_version",config_version])\r
+ if "VC_TTS_voice_type" in data["data"]:\r
+ self.VC_TTS_voice_type = data["data"]["VC_TTS_voice_type"]\r
+ EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])\r
+ print("配置VC_TTS_voice_type成功")\r
+ if "server_ip" in data["data"]:\r
+ self.server_ip = data["data"]["server_ip"]\r
+ EventMesh.publish("set_storage_config",["server_ip",self.server_ip])\r
+ print("配置server_ip成功")\r
+ if "power_on_tone" in data["data"]:\r
+ self.power_on_tone = data["data"]["power_on_tone"]\r
+ EventMesh.publish("set_storage_config",["power_on_tone",self.power_on_tone])\r
+ print("配置power_on_tone成功")\r
+ if "dialog_attributes_provider" in data["data"]:\r
+ self.dialog_attributes_provider = data["data"]["dialog_attributes_provider"]\r
+ EventMesh.publish("set_storage_config",["dialog_attributes_provider",self.dialog_attributes_provider])\r
+ if "outbound_attributess_provider" in data["data"]:\r
+ self.outbound_attributess_provider = data["data"]["outbound_attributess_provider"]\r
+ EventMesh.publish("set_storage_config",["outbound_attributess_provider",self.outbound_attributess_provider])\r
+ print("配置outbound_attributess_provider成功")\r
+ if "inbound_attributess_provider" in data["data"]:\r
+ self.inbound_attributess_provider = data["data"]["inbound_attributess_provider"]\r
+ EventMesh.publish("set_storage_config",["inbound_attributess_provider",self.inbound_attributess_provider])\r
+ print("配置inbound_attributess_provider成功")\r
+ if "speech_rate" in data["data"]:\r
+ self.speech_rate = EventMesh.publish("SET_SPEECH_RATE",float(data["data"]["speech_rate"]))\r
+ EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])\r
+ print("配置speech_rate成功")\r
+ if "emotion" in data["data"]:\r
+ self.emotion = data["data"]["emotion"]\r
+ EventMesh.publish("set_storage_config",["emotion",self.emotion])\r
+ print("配置emotion成功")\r
+ if "ota_update_notice" in data["data"]:\r
+ self.ota_update_notice =data["data"]["ota_update_notice"]\r
+ EventMesh.publish("set_storage_config",["ota_update_notice",self.ota_update_notice])\r
+ print("配置ota_update_notice成功")\r
+ if "VC_TTS_volume" in data["data"]:\r
+ self.VC_TTS_volume =int(data["data"]["VC_TTS_volume"])\r
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
+ print("配置VC_TTS_volume成功")\r
+ \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ else:\r
+ print("status_code:{}".format(response.status_code))\r
+ print("http_get_config:error!!!!!!!!!!")\r
+ response.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if response != None:\r
+ response.close()\r
+\r
+ def audio_cb(self,event):\r
+ if event == 0:\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ self.audio_play_state =1\r
+ #EventMesh.publish("set_audio_state", 1)\r
+ print('audio-play start.')\r
+ # 本地 aud.play(开录/思考短音等)不改动 AI 灯,仅 TTS 流由 VC_Play_Start 控绿灯\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[LED] audio_cb start skip (local_play)")\r
+ return\r
+ elif event == 7:\r
+ self.audio_play_state = 0\r
+ #EventMesh.publish("set_audio_state", 0) \r
+ print('audio-play finish.!!!!!!')\r
+ try:\r
+ if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:\r
+ self._vc_publish_led("thinking", "audio_cb/finish/ptt_await")\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_record_state_flag", 0) != 0:\r
+ self._vc_publish_led("listening", "audio_cb/finish/recording")\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._vc_tts_playback_active():\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[LED] audio_cb finish skip (tts_playback)")\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "_ptt_led_lock_listening", 0) == 1:\r
+ return\r
+ except Exception:\r
+ pass\r
+ self._vc_led_sync_from_fsm()\r
+ def set_tts_volume(self,event,msg):\r
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",msg])#音量配置\r
+ self.VC_TTS_volume = msg\r
+ print("set_tts_volume:{} {}".format(self.VC_TTS_volume,msg))\r
+ if int(msg) <= 10: #最小一定要设置1要不然没有声音\r
+ volume = 10 //10\r
+ self.aud.setVolume(volume)\r
+ else:\r
+ self.aud.setVolume(self.VC_TTS_volume //10)\r
+ pass\r
+ def get_yuyinaudio_flag(self, event=None, msg = None):\r
+ return self.voice_flag\r
+\r
+ def set_yuyinaudio_flag(self, event=None, msg = None):\r
+ self.voice_flag=0\r
+\r
+ #留言主动拒听\r
+ def get_pwk_cnt_voice(self, event=None, msg = None):\r
+ return self.pwk_cnt_voice\r
+\r
+ def set_pwk_cnt_voice(self, event=None, msg = None):\r
+ self.pwk_cnt_voice=msg\r
+ print("设置未读次数为:{}".format(self.pwk_cnt_voice))\r
+\r
+ #会议\r
+ def get_meetaudio_flag(self, event=None, msg = None):\r
+ return self.meet_flag\r
+ def set_meetaudio_flag(self, event=None, msg = None):\r
+ self.meet_flag = 0\r
+\r
+ def get_audio_play_state(self, event=None, msg = None):\r
+ return self.audio_play_state\r
+ \r
+ \r
+ \r
+ def come_meet_mode(self, event=None, msg = None):\r
+ if self.meet_flag == 0: #还是为0就提示进入会议纪要模式\r
+ EventMesh.publish("send_meet_open_flag_to_iot")#创建会议\r
+ self.meet_flag=1\r
+ exit_tts ="开启会议纪要,正在录音"\r
+ EventMesh.publish("play_tts",[exit_tts,1])\r
+ utime.sleep_ms(4000)\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press}) \r
+ pass\r
+ def exit_meet_mode(self, event=None, msg = None):\r
+ if self.meet_flag == 1: #还是为1就提示已退出会议纪要模式\r
+ EventMesh.publish("send_meet_end_flag_to_iot")#结束会议\r
+ self.meet_flag=0\r
+ EventMesh.publish("stop_voice_record") #关闭语音录音\r
+ exit_tts ="录音结束,已退出会议纪要模式"\r
+ EventMesh.publish("play_tts",[exit_tts,1])\r
+ pass\r
+ def come_trans_mode(self, event=None, msg = None):\r
+ if self.trans_flag == 0: #还是为0就提示进入AI翻译模式\r
+ EventMesh.publish("send_trans_come_flag_to_iot")#进入翻译\r
+ pass\r
+ def exit_trans_mode(self, event=None, msg = None):\r
+ if self.trans_flag == 1: #还是为1就提示已退出AI翻译模式 \r
+ EventMesh.publish("stop_voice_record") #关闭语音录音\r
+ exit_tts ="已退出AI翻译模式"\r
+ EventMesh.publish("play_tts",[exit_tts,1])\r
+ self.trans_flag=0\r
+ EventMesh.publish("send_trans_end_flag_to_iot")#退出翻译\r
+ pass\r
+ def set_trans_flag(self,event,msg):\r
+ self.trans_flag = msg\r
+ def exit_voice_message_mode(self, event=None, msg = None):\r
+ if self.voice_flag == 1: #还是为1就提示已退出会议纪要模式\r
+ self.voice_flag=0\r
+ exit_tts ="已退出语音留言模式"\r
+ EventMesh.publish("play_tts",[exit_tts,1])\r
+ pass\r
+ def ai_key_press_audio_cb1(self, event=None, msg = None):\r
+ print("播放按键音!!!!!!!!!!")\r
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
+ # self.chenggong_tone()\r
+ self.aud.aud_tone_play(10,400)#播放按键音\r
+ def ai_key_press_audio_cb2(self, event=None, msg = None):\r
+ EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
+ # self.chenggong_tone() #播放按键音\r
+\r
+ def AI_key_press(self, event=None, msg = None):#按键按下\r
+ # 按键时打印 LED 聚合状态/最终模式(用于定位“绿灯不灭/闪一下”)\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):\r
+ try:\r
+ led = EventMesh.publish("get_led_debug_state")\r
+ except Exception:\r
+ led = None\r
+ try:\r
+ t = getattr(self.fsm, "turn", None)\r
+ except Exception:\r
+ t = None\r
+ print("[KEY] AI_key_press VC_mode={} {} turn={} led={}".format(\r
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))\r
+ except Exception:\r
+ pass\r
+ # 单对话 PTT / 长对话分支(按键短按提示音已关,避免与开录等提示音叠在一起)\r
+ try:\r
+ if self._ai_key_press_ptt_mode():\r
+ return\r
+ except Exception:\r
+ pass\r
+ if self._ai_key_press_long_dialogue_interrupt():\r
+ return\r
+ try:\r
+ if self._ai_key_press_long_dialogue_when_session_ready():\r
+ return\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if self._ai_key_press_long_dialogue_wake():\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
+ return\r
+ except Exception:\r
+ pass\r
+\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ if EventMesh.publish("get_sos_flag")== 1:\r
+ return\r
+ if self.voice_flag == 1: #语音留言模式\r
+ self.stop_voice_timer.stop() #关闭语音留言模式定时器\r
+ # EventMesh.publish("stop_AI_record")\r
+ self.Close()\r
+ self.revoice_cnt = 0\r
+ self.revoice_timer.stop()#回复定时器关闭,计数器清零\r
+ EventMesh.publish("start_voice_record",2)#开始语音录音\r
+ return\r
+ elif self.meet_flag == 1:#会议纪要模式\r
+ self.Close()\r
+ EventMesh.publish("start_voice_record",3)#开始会议录音\r
+ return\r
+ elif self.trans_flag == 1:#AI翻译模式\r
+ return\r
+ \r
+ EventMesh.publish("stop_voice_record") #关闭语音录音\r
+ print("AI_key_press: unhandled mode branch")\r
+ self.AI_key_long_press_flag = 0\r
+ def BT_AI_key_press(self, event=None, msg = None):#按键按下\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ if EventMesh.publish("get_sos_flag")== 1:\r
+ return\r
+ if self.voice_flag == 1: #语音留言模式\r
+ tts_str = "语音留言模式,长按按钮开始录音,松开按钮结束" \r
+ EventMesh.publish("play_tts",[tts_str,1])\r
+ return\r
+ \r
+ EventMesh.publish("stop_voice_record") #开启静默需要关闭\r
+ print("AI_key_press")\r
+ self.get_config_timer_count = 0\r
+ self.key_state = 1\r
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
+ # self.chenggong_tone() #播放按键音\r
+ # 长对话:蓝牙按键同单按\r
+ # - 对话进行中:打断/续聊(复用 dialog_id)\r
+ # - 已停止/未 started:走新会话流程\r
+ try:\r
+ if self.VC_mode == 0:\r
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})\r
+ return\r
+ except Exception:\r
+ pass\r
+\r
+ self.Close()\r
+ \r
+ if self.VC_mode == 1: #长按对话模式 接收到蓝牙耳机按键事件\r
+ \r
+ tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
+ EventMesh.publish("play_tts",[tts_str,1])\r
+ return\r
+ # if dev_type == "7PIN":\r
+ # self.chenggong_tone()\r
+ #self.aud.aud_tone_play(10,200)#播放按键音\r
+ self.AI_TTS_str = ""#"你好,有什么吩咐"\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ # if self.VC_state != VC_State_None:\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})\r
+ self._uplink_request_stop("BT_AI_key_press/compat2")\r
+ self._uplink_request_start("BT_AI_key_press/compat2")\r
+ \r
+ self.AI_VC_start() \r
+ \r
+ \r
+ # self.Close()\r
+ # self.AI_VC_start()\r
+ self.AI_key_long_press_flag = 0\r
+ def stop_AI_record(self, event=None, msg=None):#停止AI对话\r
+ if self.VC_satrt_flag == 1:\r
+ self.get_config_timer_count = 0\r
+ self.AI_TTS_str = ""#"你好,有什么吩咐"\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ self.AI_key_long_press_flag = 0\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})\r
+ self.request_uplink_stop("stop_AI_record")\r
+ count = 0\r
+ while True:\r
+ if count >= 20 or self.VC_satrt_flag == 0:\r
+ break\r
+ count+=1\r
+ utime.sleep_ms(500)\r
+\r
+ def exit_voice_message_timer(self,*argc):\r
+ EventMesh.publish("exit_voice_message_mode")\r
+\r
+ def AI_key_release(self, event=None, msg=None):#按键弹起\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):\r
+ try:\r
+ led = EventMesh.publish("get_led_debug_state")\r
+ except Exception:\r
+ led = None\r
+ try:\r
+ t = getattr(self.fsm, "turn", None)\r
+ except Exception:\r
+ t = None\r
+ print("[KEY] AI_key_release VC_mode={} {} turn={} led={}".format(\r
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))\r
+ except Exception:\r
+ pass\r
+ print("AI_key_release123")\r
+ self.key_state = 0\r
+ if self.trans_flag == 1:#AI翻译模式\r
+ EventMesh.publish("stop_voice_record") #关闭语音录音\r
+ return\r
+ if self.voice_flag == 1:\r
+ self.stop_voice_timer.stop()\r
+ self.voice_flag=0\r
+ if EventMesh.publish("GET_USB_STATE") == 0:\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
+ EventMesh.publish("stop_voice_record")#停止静默录音\r
+ tts_play_str = "留言已发送"\r
+ self.voice_flag=0\r
+ EventMesh.publish("play_tts",[tts_play_str,1])\r
+ return\r
+ if self.VC_mode == 1:\r
+ self._ai_key_release_handle_ptt()\r
+ return\r
+ def pwk_key_double_press(self,event,msg):#pwk双击\r
+ print("come 双击")\r
+ # 切换模式:必须打断当前对话/录音/播放,回到 idle,等待按键触发新一轮\r
+ try:\r
+ self.Close_flag = 1\r
+ except Exception:\r
+ pass\r
+ try:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
+ except Exception:\r
+ pass\r
+ if self.VC_mode == 0:\r
+ self.VC_mode = 1\r
+ tts_str = "已切换到单对话模式" \r
+ # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(单对话)\r
+ try:\r
+ self._pending_tts_kind = "mode_switch_ptt"\r
+ self._last_tts_kind = "mode_switch_ptt"\r
+ except Exception:\r
+ pass\r
+ else:\r
+ self.VC_mode = 0 \r
+ tts_str = "已切换到长对话模式" \r
+ # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(长对话)\r
+ try:\r
+ self._pending_tts_kind = "mode_switch_long"\r
+ self._last_tts_kind = "mode_switch_long"\r
+ except Exception:\r
+ pass\r
+ # 提示音播完即退出(Close_flag=1),保持 idle,等待按键\r
+ EventMesh.publish("play_tts",[tts_str,1])\r
+ EventMesh.publish("set_storage_config",["VC_mode",self.VC_mode])\r
+\r
+ def AI_key_long_press(self, event, msg):#按键长按\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):\r
+ try:\r
+ led = EventMesh.publish("get_led_debug_state")\r
+ except Exception:\r
+ led = None\r
+ try:\r
+ t = getattr(self.fsm, "turn", None)\r
+ except Exception:\r
+ t = None\r
+ print("[KEY] AI_key_long_press VC_mode={} {} turn={} led={}".format(\r
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))\r
+ except Exception:\r
+ pass\r
+ print("AI_key_long_press")\r
+ self._ai_key_long_press_route(event, msg)\r
+\r
+ def pwk_key_release(self, event, msg):#电源键短按,关闭\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ _m = bool(vc_config.get_bool("SINGLE_POWER_KEY_AI_MERGED", False))\r
+ if not _m:\r
+ _ai = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22")\r
+ _m = _ai is None or _ai == ""\r
+ else:\r
+ _m = False\r
+ except Exception:\r
+ _m = False\r
+ if _m:\r
+ self._handle_pwk_short_release(event, msg, merged_ai_tail=True)\r
+ return\r
+ self._handle_pwk_short_release(event, msg, merged_ai_tail=False)\r
+\r
+\r
+ def key_prompt(self):\r
+ EventMesh.publish("audio_montage_play",["key_prompt.mp3"])\r
+ #self.aud.play(1,1,'U:/static/key_prompt.mp3')#提示请您按住中间的按钮开始讲话,讲完松开即可\r
+ def VC_error(self, event, msg):#错误播报\r
+ self.websocket_error()\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error}) \r
+ def set_VC_record_state_flag(self,data):\r
+ \r
+ self.VC_record_state_flag = data\r
+ print("[VC] record_state_flag={}".format(self.VC_record_state_flag))\r
+ #开始录音,不缓存,直接发\r
+ def VC_Speech_Start_now(self):\r
+ # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)\r
+ self._server_vad_silence = 0\r
+ self.VC_Speech_Start()\r
+ if self.VC_record_state_flag == 1:\r
+ self.VC_record_state_flag =2\r
+ print("[VC] Speech_Start_now record_state={}".format(self.VC_record_state_flag))\r
+ #开始录音,先缓存到buf,等服务器响应再发\r
+ def VC_Speech_Start(self):\r
+ try:\r
+ \r
+ #self.VC_state = VC_State_SpeechStart\r
+ print("[VC] Speech_Start record_state={} AI_ASR_flag={}".format(self.VC_record_state_flag,self.AI_ASR_flag))\r
+ if self.AI_ASR_flag != 0:\r
+ return\r
+ \r
+ if self.VC_record_state_flag == 0:\r
+ # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)\r
+ self._server_vad_silence = 0\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ self._last_asr_result_text = ""\r
+ try:\r
+ self._asr_completed_tone_done = 0\r
+ except Exception:\r
+ pass\r
+ \r
+ self.VC_record_thread_close()#关闭录音线程\r
+ _thread.stack_size(self.Audio_buf_size+8*1024)\r
+ self.VC_record_thread_id = _thread.start_new_thread(self.VC_record_thread,())\r
+ _thread.stack_size(8*1024)\r
+ print("self.VC_record_thread_id:{}".format(self.VC_record_thread_id))\r
+ try:\r
+ \r
+ if self.VC_record_TYPE == VC_record_TYPE_AMR :\r
+ try :\r
+ self.record.stream_start(audio.Record.AMRNB, self.VC_record_samplerate, 0)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ elif self.VC_record_TYPE == VC_record_TYPE_PCM:\r
+ print("record_pcm.stream_read_start()!!!!!!!!!!!!")\r
+ self.record.stream_start(audio.Record.WAV, self.VC_record_samplerate, 0)\r
+ # if self.record_pcm != None:\r
+ # try:\r
+ # self.record_pcm.stream_stop()\r
+ # self.record_pcm.close()\r
+ # self.record_pcm = None\r
+ # except Exception as e:\r
+ # usys.print_exception(e)\r
+ # if self.record_pcm == None: \r
+ # ret = self.record_pcm = audio.Audio.PCM(0,1,16000,0,1,2)\r
+ # print("audio.Audio.PCM:ret:{}".format(ret))\r
+ # ret = self.record_pcm.stream_read_start()\r
+ # print("self.record_pcm.stream_read_start:ret:{}".format(ret))\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ \r
+ self.VC_record_state_flag = 1\r
+ # 录音硬件已起:协议侧应对应 audio.create(PTT)或长对话直传 PCM\r
+ try:\r
+ print("[VC][proto] record_start sr={} pcm_type={} VC_mode={} session_started={} "\r
+ "ptt_audio_created={} thread_id={}".format(\r
+ self.VC_record_samplerate, self.VC_record_TYPE, self.VC_mode,\r
+ getattr(self, "_vc_session_started", -1),\r
+ getattr(self, "_ptt_audio_created", -1),\r
+ self.VC_record_thread_id))\r
+ except Exception:\r
+ pass\r
+ # 录音开始:聆听灯(PTT 蓝快闪 / 长对话蓝慢闪)\r
+ try:\r
+ self._vc_publish_led("listening", "VC_Speech_Start")\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ self._ptt_on_record_hardware_started()\r
+ except Exception:\r
+ pass\r
+ \r
+ except Exception as e:\r
+ self.websocket_error()\r
+ self.VC_record_state_flag = 1\r
+ self.VC_Speech_End()#关闭录音\r
+ usys.print_exception(e) \r
+ #停止录音\r
+ def VC_Speech_End(self):\r
+ try:\r
+ self._uplink_mark_end("VC_Speech_End")\r
+ self.AI_ASR_flag = 0\r
+ self.AI_ASR_str_tmp = ""\r
+ # 勿在此清空 AI_ASR_str:vad.silence 停麦常早于 asr.completed,清空会导致 check_text:len:0 连播 think 音\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr})\r
+ \r
+ if self.VC_record_state_flag == 0:\r
+ return\r
+ #self.AI_client_asr_StopTranscription_requset()#请求停止\r
+ if self.VC_record_TYPE == VC_record_TYPE_AMR:\r
+ self.record.stream_stop()\r
+ self.record.stop()#关闭录音\r
+ Record_read_buf = bytearray(self.Audio_buf_size)\r
+ for i in range(10):\r
+ Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
+ # if VC_AUDIO_FRAME_LOG:\r
+ # print("Record_len:!!!!!!{}".format(Record_len))\r
+ if Record_len <= 0:\r
+ break\r
+ elif self.VC_record_TYPE == VC_record_TYPE_PCM:\r
+ self.record.stream_stop()\r
+ self.record.stop()#关闭录音\r
+ Record_read_buf = bytearray(self.Audio_buf_size)\r
+ for i in range(10):\r
+ Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
+ # if VC_AUDIO_FRAME_LOG:\r
+ # print("Record_len:!!!!!!{}".format(Record_len))\r
+ if Record_len <= 0:\r
+ break\r
+ # if self.record_pcm != None:\r
+ # try:\r
+ # self.record_pcm.stream_stop()\r
+ # self.record_pcm.close()\r
+ # self.record_pcm = None\r
+ # except Exception as e:\r
+ # usys.print_exception(e)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.VC_record_thread_close()#关闭录音线程\r
+ if self.AI_record_tmp_queue.empty() != True:\r
+ self.AI_record_tmp_queue.clear()\r
+ \r
+ self.VC_record_state_flag = 0\r
+ # PTT 一轮录音结束:清长按语义,避免后续短按松手误走 end_speech 而不播提示音\r
+ try:\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ self.AI_key_long_press_flag = 0\r
+ except Exception:\r
+ pass\r
+ # 录音停止:单对话进入思考态(等服务器),勿继续蓝闪聆听\r
+ try:\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ self._ptt_await_server = 1\r
+ self._vc_publish_led("thinking", "VC_Speech_End/ptt")\r
+ else:\r
+ self._ptt_await_server = 0\r
+ self._vc_publish_led("idle", "VC_Speech_End")\r
+ except Exception:\r
+ pass\r
+ print("[VC] Speech_End record_state={}".format(self.VC_record_state_flag))\r
+ #关闭录音线程 \r
+ def VC_record_thread_close(self): \r
+ if self.VC_record_thread_id != 0:\r
+ count =50\r
+ while True:\r
+ count -= 1\r
+ if self.VC_record_thread_lock == 1:\r
+ self.VC_record_thread_lock = 2\r
+ if self.VC_record_thread_lock == 0 or count <=0:\r
+ break\r
+ else:\r
+ print("VC_record_thread_thread going:{}...".format(count))\r
+ utime.sleep_ms(1)\r
+ self.VC_record_thread_lock = 0\r
+ print("VC_record_thread_thread_close")\r
+ if self.VC_record_thread_id != 0:\r
+ try:\r
+ _thread.stop_thread(self.VC_record_thread_id)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.VC_record_thread_id = 0\r
+ #获取录音数据\r
+\r
+ def VC_record_thread(self):\r
+ ##return\r
+ print("[VC] record_thread start")\r
+ count = 0\r
+ RequestToSpeak_flag = 0\r
+ # try:\r
+ # uos.remove('/usr/test.pcm')\r
+ # except Exception as e:\r
+ # usys.print_exception(e)\r
+ #record_data_queue = Queue(100)\r
+ #fd = open('/usr/test.pcm','w')\r
+ # if self.VC_record_TYPE == 1:\r
+ # h_data= [0x23,0x21 ,0x41 ,0x4D ,0x52 ,0x0A ]\r
+ # h_data_b = bytes(h_data)\r
+ # self.AI_client_VC_send(h_data_b)\r
+ AI_record_tmp_queue_len = 0\r
+ vad_buf = bytearray()\r
+ VAD_data_count = 0\r
+ vad_state = 0\r
+ self.VAD_count = 0\r
+ while True:\r
+ try:\r
+ if self.VC_record_thread_lock == 2:\r
+ break\r
+ self.VC_record_thread_lock = 1\r
+ # 死循环接收数据\r
+ #if self.VC_state == VC_State_SpeechStart:\r
+ Record_read_buf = bytearray(self.Audio_buf_size)\r
+ \r
+ Record_len = 0\r
+ if self.VC_record_TYPE == VC_record_TYPE_PCM:#pcm\r
+ Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
+ #utime.sleep_ms(60)\r
+ \r
+ else :\r
+ \r
+ Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
+ utime.sleep_ms(600)\r
+\r
+ #Record_len = 1024*2\r
+ #print("VC_record_thread:{}".format(Record_len))\r
+ if Record_len > 0:\r
+ read_buf = bytes(Record_read_buf[:Record_len])\r
+ # if VC_AUDIO_FRAME_LOG:\r
+ # print("Record_len:{}".format(Record_len))\r
+ if self.record_data_save_flag == 1:\r
+ if self.record_fd == None:\r
+ try:\r
+ uos.remove('/usr/record_data.pcm')\r
+ except Exception as e:\r
+ pass\r
+ # 二进制写入,避免文本模式导致数据异常\r
+ self.record_fd = open('/usr/record_data.pcm', 'wb')\r
+ self.record_fd_len = 0\r
+ if self.record_fd != None:\r
+ try:\r
+ self.record_fd.write(read_buf)\r
+ print("write record:{}".format(len(read_buf)))\r
+ self.record_fd_len = self.record_fd_len+len(read_buf)\r
+ if self.record_fd_len >=500*1024:\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ # count+=Record_len\r
+ # if count >150*1024:\r
+ # self.record_fd.close()\r
+ #self.record_fd = None\r
+ #VAD检测\r
+ if self.is_VAD == 1:\r
+ vad_state = 0\r
+ tmp_c = 1000\r
+ while True:\r
+ tmp_c -=1\r
+ if tmp_c <=0:\r
+ break\r
+ #vad_buf_len = len(vad_buf)\r
+ if VAD_data_count == 0:#没数据的时候\r
+ vad_buf = Record_read_buf[:self.VAD_data_len]\r
+ VAD_data_count = self.VAD_data_len\r
+ self.vad.custom_vad_reset()#重置vad\r
+ else:#有数据的时候\r
+ if Record_len - VAD_data_count < self.VAD_data_len :#剩余长度不够一次填充,先保留,但不填算法\r
+ vad_buf = Record_read_buf[VAD_data_count:Record_len]\r
+ VAD_data_count = 0\r
+ break\r
+ else:#剩余长度够填一次数据\r
+ vad_buf = Record_read_buf[VAD_data_count:VAD_data_count+self.VAD_data_len]\r
+ VAD_data_count += len(vad_buf)\r
+ #print("vad_buf_len:{},{}".format(len(vad_buf),VAD_data_count))\r
+ \r
+ if len(vad_buf) >= self.VAD_data_len:\r
+ vad_state = self.vad.custom_vad_process(vad_buf,len(vad_buf))#给vad数据\r
+ #print("vad_state:{}".format(vad_state))\r
+ \r
+ if VAD_data_count >= Record_len:\r
+ VAD_data_count = 0\r
+ break\r
+ if vad_state == 1:\r
+ print("检测到VAD=1!!!!!!!!!!!!!")\r
+ \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断\r
+ # RequestToSpeak_flag = 1\r
+ \r
+ self.VAD_count+=1\r
+ if self.VAD_count >= 20: \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断\r
+ \r
+ else:\r
+ if self.VAD_count != 0:\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Pause,"data":0})#恢复音频播放\r
+ self.VAD_count = 0\r
+ if self._vc_tts_pipeline_busy():\r
+ pass\r
+ if vad_state == 1:\r
+ self.AI_record_tmp_queue.put(read_buf)#入缓存队列\r
+ AI_record_tmp_queue_len += len(read_buf)\r
+ \r
+ \r
+ else:\r
+ VAD_data_count = 0\r
+ if self.VC_record_state_flag <=1 :#没发送前先存起来\r
+ self.AI_record_tmp_queue.put(read_buf)\r
+ AI_record_tmp_queue_len += len(read_buf)\r
+ else:\r
+ AI_record_tmp_queue_len = 0\r
+ while True:\r
+ if self.AI_record_tmp_queue.empty() == True:\r
+ break\r
+ else:\r
+ pass\r
+ # 协议门控:会话未 started 前禁止上行音频(否则 session not initialized)\r
+ if VC_PROTO_GUARDS and self._vc_session_started != 1 and self.record_data_save_flag == 0:\r
+ _ = self.AI_record_tmp_queue.get()\r
+ AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(_))\r
+ continue\r
+ if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):\r
+ self.AI_client_VC_send(self.AI_record_tmp_queue.get()) #发送之前存的数据\r
+ else:\r
+ self._uplink_mark_end("silence_gate(drop_queue)")\r
+ _ = self.AI_record_tmp_queue.get()\r
+ # 当前帧发送前协议门控(可用 VC_PROTO_GUARDS 临时关闭):\r
+ # - session started 前禁止发帧\r
+ # - Push-To-Talk 在 audio.create 前禁止发帧\r
+ if VC_PROTO_GUARDS and (\r
+ (self._vc_session_started != 1 and self.record_data_save_flag == 0) or\r
+ (self.VC_mode == 1 and self._ptt_audio_created != 1 and self.record_data_save_flag == 0)\r
+ ):\r
+ # 未 ready:转入缓存,等待 started 再发(并做容量控制)\r
+ try:\r
+ self.AI_record_tmp_queue.put(read_buf)\r
+ AI_record_tmp_queue_len += len(read_buf)\r
+ while AI_record_tmp_queue_len > 128*1024 and self.AI_record_tmp_queue.empty() != True:\r
+ d = self.AI_record_tmp_queue.get()\r
+ AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(d))\r
+ except Exception:\r
+ pass\r
+ else:\r
+ if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):\r
+ self.AI_client_VC_send(read_buf)\r
+ else:\r
+ self._uplink_mark_end("silence_gate(drop_buf)")\r
+ \r
+ \r
+\r
+ \r
+ \r
+\r
+ #print("VC_record_thread:{}".format(len(read_buf)))\r
+ #count+=len(read_buf)\r
+ # if count>=500*1024:#结束\r
+ # self.AI_client_VC_end_speech()\r
+ # break\r
+ # count+=1\r
+ # if count %10 == 0:\r
+ # print("VC_record_thread on line {} asr recv :{} send:{}".format(self.VC_record_state_flag,_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)))\r
+ \r
+ # if self.VC_state >= VC_State_SpeechEnd:\r
+ # break\r
+ \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ #break\r
+ self.VC_record_thread_lock = 0\r
+ if self.record_data_save_flag == 1:\r
+ utime.sleep_ms(1)\r
+ else:\r
+ utime.sleep_ms(100)\r
+ #fd.close()\r
+ print("VC_record_thread:exit")\r
+ self.VC_record_thread_id = 0#线程退出\r
+ self.VC_record_thread_lock = 0\r
+###################################################### VC ###################################################################################\r
+ def websocket_connect_VC(self):\r
+ print("websocket_connect_VC:{} e:{}!!!!!!!!!!!!!!!!!!!!!".format(self.AI_client_VC_isconnect,self.AI_client_VC_debug))\r
+ if self.AI_client_VC_isconnect == 1:\r
+ return\r
+ # 创建VC客户端\r
+ try:\r
+ signal = net.csqQueryPoll()\r
+ if signal == -1 or sim.getStatus() != 1:#无蜂窝/无卡等:弱网提示\r
+ self.websocket_error(play_weak_signal_tone=True)\r
+ return\r
+ \r
+ req_str = ""\r
+ ack = ""\r
+ if self.is_VC_ACK == 1:\r
+ ack = "ack=1"\r
+ lang = "lang=zh-cn"#普通话测试环境\r
+ try:\r
+ if "lang" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
+ lang = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["lang"]\r
+ else:\r
+ print("not lang!!!!!!!!!!!!!!!!!!!!!!")\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ \r
+ #女,男,童,粤,川\r
+ self.bot_name = "柒先生"\r
+ try:\r
+ if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
+ self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]\r
+ else:\r
+ print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ \r
+ # Headers = {'X-NLS-Token':'0c8b8f05a46f423096be64a9c4c46310'}\r
+ # self.AI_client_VC = uwebsocket.Client.connect('wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1',headers=Headers, debug=False)\r
+ token = EventMesh.publish("get_dev_token")\r
+ print("token:{}".format(token))\r
+ \r
+ req_str = "vc/chat?is_auth={}".format(self.is_auth)\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ base = vc_config.get_attr("VC_WS_PATH", "vc/chat") or "vc/chat"\r
+ req_str = "{}?is_auth={}".format(base, self.is_auth)\r
+ except Exception:\r
+ pass\r
+ if(token):\r
+ url = 'ws://{}/{}&dt={}'.format(self.server_ip,req_str,token)\r
+ else:\r
+ url = 'ws://{}/{}'.format(self.server_ip,req_str)\r
+ #url = 'ws://115.190.94.208:22080/vc/coze?dt={}'.format(token)\r
+ \r
+ if VC_WS_IO_LOG:\r
+ print("[VC] ws url {}".format(url))\r
+ self.AI_client_VC = uwebsocket.Client.connect(url, debug=False)\r
+ #self.AI_WS_connect_Queue.put(self.AI_client_VC)\r
+\r
+ \r
+ print("[VC] websocket_connect ok")\r
+ except Exception as e: #网络连接异常(已通过 CSQ/SIM,多为 URL/握手/服务端问题,勿播「信号弱」)\r
+ usys.print_exception(e)\r
+ print("websocket_connect_VC error!!!\r\n")\r
+ try:\r
+ print("[VC] ws connect exc: {}".format(e))\r
+ except Exception:\r
+ pass\r
+ str_error = str(e)\r
+ if str_error.find("[Errno 22] EINVAL")!= -1: \r
+ print("[VC] websocket EINVAL: {}".format(str_error))\r
+ self.websocket_error(play_weak_signal_tone=False)\r
+ EventMesh.publish("set_storage_config",["powerRestart",1])#调试临时关闭\r
+ utime.sleep_ms(1000)\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ Power.powerRestart()\r
+ \r
+ self.websocket_error(play_weak_signal_tone=False)\r
+ return\r
+ #print("AI_client_VC:{}".format(self.AI_client_VC))\r
+ if self.AI_client_VC == None:\r
+ EventMesh.publish("update_dev_token")#获取token\r
+ self.websocket_error(play_weak_signal_tone=False)\r
+ return\r
+ self.AI_client_VC_debug+=1\r
+ utime.sleep_ms(10)\r
+ self.AI_client_VC.settimeout(5)\r
+ #self.AI_VC_send_data_queue_thread_close()#关闭发送线程\r
+ if self.AI_VC_send_data_queue_thread_id == 0: \r
+ _thread.stack_size(self.Audio_buf_size+8*1024)\r
+ self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())\r
+ _thread.stack_size(8*1024)\r
+ print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))\r
+ self.AI_client_VC_recv_thread_close()#关闭读线程\r
+ # 线程接收数据\r
+ _thread.stack_size(self.Audio_buf_size+8*1024)\r
+ self.AI_client_VC_recv_id = _thread.start_new_thread(self.AI_client_VC_recv, (self.AI_client_VC,))\r
+ _thread.stack_size(8*1024)\r
+ self.AI_client_VC_isconnect = 1\r
+ print("websocket_connect_VC end")\r
+\r
+ def websocket_error(self, TTS_str=None, play_weak_signal_tone=False):\r
+ """play_weak_signal_tone=True 时播 xinhaoruo1(弱网/无卡类);连接/协议失败请传 False,避免误报「信号弱」。"""\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] websocket_error enter VC_mode={} TTS_str={} weak_hint={} -> Close then uplink_stop(async)".format(\r
+ getattr(self, "VC_mode", -1), TTS_str, play_weak_signal_tone))\r
+ except Exception:\r
+ pass\r
+ print("websocket_error1!!!!")\r
+ utime.sleep_ms(800)\r
+ self.Close()\r
+ print("websocket_error1.1!!!!")\r
+ print("websocket_error!!!!")\r
+ print("websocket_error!!!!")\r
+ self.VC_Thinking_timerout = 0\r
+ self.VC_satrt_flag = 0\r
+ self.AI_TTS_str = ""\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ self.AI_ASR_flag = 0\r
+ # 可能由收包线程/其它线程触发:统一切回主线程执行 stop,避免“异常→重试→再异常”的循环\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] websocket_error schedule uplink_stop reason=websocket_error + set_led_idle")\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"websocket_error"})\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
+ if TTS_str == None:\r
+ self.websocket_error_count+=1\r
+ # if self.websocket_error_count <=3:\r
+ # EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])\r
+ if play_weak_signal_tone and self.websocket_error_count < 10:\r
+ EventMesh.publish("audio_montage_play", ["xinhaoruo1.mp3"])\r
+ elif self.websocket_error_count >=10:\r
+ # EventMesh.publish("paly_iccid_imei") \r
+ EventMesh.publish("audio_montage_play", ["xinhaoruo1.mp3"])\r
+ # else:\r
+ # EventMesh.publish("audio_montage_play",["R_SIM_FAILE.mp3"])\r
+ #self.aud.play(1,1,'U:/static/R_SIM_FAILE.mp3')\r
+ if self.websocket_error_count <3 :\r
+ utime.sleep_ms(3000)\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})\r
+ else:\r
+ print("websocket_error:tts:{}".format(TTS_str))\r
+ self.AI_TTS_str = TTS_str\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})\r
+ self.Close_flag = 1\r
+ print("websocket_error2{}".format(self.websocket_error_count))\r
+ \r
+ \r
+ print("websocket_error4")\r
+ \r
+\r
+ def websocket_deconnect_VC(self):\r
+ #print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))\r
+ print("websocket_deconnect_VC!!!!!!!")\r
+ self._vc_session_started = 0\r
+ # 断链/模式切换时必须重置“上行录音状态”,否则会被 _uplink_request_start 的幂等门控挡住,\r
+ # 表现为:切回长对话后无法自动开麦,按键 cancel/canceled 也无法恢复。\r
+ try:\r
+ self.VC_record_state_flag = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._server_vad_silence = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._ptt_audio_created = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ # 若录音线程/录音设备仍在占用,先停掉\r
+ self.VC_Speech_End()\r
+ except Exception:\r
+ try:\r
+ self.VC_record_thread_close()\r
+ except Exception:\r
+ pass\r
+ # 断链必须同步重置 FSM,否则 await_played/turn 残留会导致动态切换后“无法自动开麦/按键无效”\r
+ try:\r
+ self.fsm.session = VCSessionState.Disconnected\r
+ self.fsm.turn = VCTurnState.Idle\r
+ self.fsm.await_played = False\r
+ self.fsm.last_tts_completed_ms = 0\r
+ self.fsm.last_llm_ms = 0\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ self.AI_client_VC_recv_thread_close()#关闭读线程\r
+ self.AI_client_VC_recv_id = 0\r
+ if self.AI_client_VC_isconnect == 1 or self.AI_client_VC!=None:\r
+ try:\r
+ self.AI_client_VC_isconnect = 0\r
+ count = 500\r
+ while True:#等待发完数据\r
+ count-=1\r
+ if count <=0 or self.AI_VC_send_data_queue_thread_lock == 0:\r
+ break\r
+ utime.sleep_ms(1)\r
+ \r
+ try:\r
+ self.AI_client_VC.close()\r
+ self.AI_client_VC_debug -=1\r
+ #self.AI_client_VC=None\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try :\r
+ self.AI_client_VC.close()\r
+ self.AI_client_VC_debug -=1\r
+ #self.AI_client_VC=None\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try :\r
+ self.AI_client_VC._close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.AI_client_VC = None\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))\r
+ #websocket发送ping\r
+ def AI_client_VC_ping(self):\r
+ pass\r
+ if self.AI_client_VC_isconnect == 1:\r
+ #self.AI_client_VC.write_frame(0x09,b'ping ping')\r
+ self.AI_client_VC_send("ping")\r
+ if VC_HEARTBEAT_LOG:\r
+ print("VC: ping ping")\r
+\r
+ #websocket发送数据 \r
+ def AI_client_VC_send(self,data_str):\r
+ # if isinstance(data_str, str):#是字符串\r
+ # print("AI_client_VC_send:{}".format(data_str))\r
+ # pass\r
+ # else:\r
+ # print("AI_client_VC_send:len{}".format(len(data_str)))\r
+ # 统计“语音上行”真实开始/结束:这里只统计二进制音频(非 ping/字符串)\r
+ try:\r
+ if not isinstance(data_str, str):\r
+ l = len(data_str)\r
+ if l > 0:\r
+ self._uplink_mark_start(l)\r
+ self._uplink_bytes += l\r
+ except Exception:\r
+ pass\r
+ self.AI_VC_send_data_queue.put([AI_client_VC_num,data_str]) \r
+ #关闭数据发送线程\r
+ def AI_VC_send_data_queue_thread_close(self):\r
+ if self.AI_VC_send_data_queue_thread_id != 0:\r
+ # 先清空待发队列,避免关闭过程持续堆积/刷屏\r
+ if self.AI_VC_send_data_queue.empty() != True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ # 通知发送线程退出;若发送线程正阻塞在底层 send,则只能等超时或强制 stop_thread\r
+ self.AI_VC_send_data_queue_thread_lock = 2\r
+ count = 500\r
+ while True:\r
+ count -= 1\r
+ if self.AI_VC_send_data_queue_thread_lock == 0 or count <=0:\r
+ break\r
+ else:\r
+ # 该打印只是“等待线程停下来”的调试心跳,默认关闭以免影响系统实时性\r
+ if VC_HEARTBEAT_LOG and (count % 50 == 0 or count > 490):\r
+ print("AI_VC_send_data_queue_thread going:{}...".format(count))\r
+ utime.sleep_ms(5)\r
+ self.AI_VC_send_data_queue_thread_lock = 0\r
+ if VC_HEARTBEAT_LOG:\r
+ print("AI_VC_send_data_queue_thread_close")\r
+ if self.AI_VC_send_data_queue_thread_id != 0:\r
+ try:\r
+ # 线程可能已自然退出/被复用,stop_thread 会抛 ValueError\r
+ try:\r
+ running = _thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)\r
+ except Exception:\r
+ running = True\r
+ if running:\r
+ _thread.stop_thread(self.AI_VC_send_data_queue_thread_id)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.AI_VC_send_data_queue_thread_id = 0\r
+ #发送数据队列\r
+ def AI_VC_send_data_queue_thread(self):\r
+ print("AI_VC_send_data_queue_thread:start")\r
+ count_t = 0\r
+ while True:\r
+ if self.AI_VC_send_data_queue_thread_lock == 2:\r
+ print("AI_VC_send_data_queue_thread,该退出了")\r
+ break \r
+ if self.AI_VC_send_data_queue.empty() :#不能去掉,去掉会卡死\r
+ self.AI_VC_send_data_queue_thread_lock = 0 \r
+ count_t+=1\r
+ # if count_t%200 == 0:\r
+ # print("self.AI_VC_send_data_queue.empty")\r
+ utime.sleep_ms(1)\r
+ else:\r
+ #data[0]是websocket连接对象编号,data[1]是数据\r
+ #print("AI_VC_send_data_queue_thread:get")\r
+ \r
+ self.AI_VC_send_data_queue_thread_lock = 1\r
+ data = self.AI_VC_send_data_queue.get()\r
+ \r
+ \r
+ #print("AI_VC_send_data_queue_thread:num{} len{}".format(data[0],len(data[1])))\r
+ if data != None:\r
+ try:\r
+ #data["func"](data["data"])\r
+ # 发送数据\r
+ #bytes_data = bytes(data[1])\r
+ #data[0].send(data[1])\r
+ #print("AI_VC_send_data_queue_thread:len:{}".format(len(data[1])))\r
+ #print("AI_VC_send_data_queue_thread on line asr recv :{} vc recv:{}".format(_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_client_VC_recv_id)))\r
+ if data[0] == AI_client_VC_num:\r
+ \r
+ if isinstance(data[1], str):#是字符串\r
+ self.VC_pingping_timerout = 0\r
+ s = data[1]\r
+ if s is None:\r
+ s = ""\r
+ # 保活 ping 不落 WS IO 日志(避免周期性刷屏)\r
+ if VC_WS_IO_LOG and s != "ping":\r
+ try:\r
+ if len(s) > 240:\r
+ preview = s[:160] + " ... " + s[-60:]\r
+ else:\r
+ preview = s\r
+ print("[VC] WS send_txt len={} txt={}".format(len(s), preview))\r
+ except Exception:\r
+ try:\r
+ print("[VC] WS send_txt {}".format(data[1]))\r
+ except Exception:\r
+ pass\r
+ elif not VC_WS_IO_LOG:\r
+ if s == "ping":\r
+ pass\r
+ else:\r
+ try:\r
+ ev, tid = _vc_out_json_event_meta(s)\r
+ if ev:\r
+ print("[VC][proto] tx event={} trace_id={} len={} ws_conn={}".format(\r
+ ev, tid or "-", len(s), self.AI_client_VC_isconnect))\r
+ else:\r
+ print("[VC][proto] tx unknown_json len={} ws_conn={}".format(\r
+ len(s), self.AI_client_VC_isconnect))\r
+ except Exception:\r
+ print("[VC][proto] tx parse_fail len={}".format(len(s)))\r
+ else:\r
+ pass\r
+ # if VC_AUDIO_FRAME_LOG:\r
+ # print("VC: send_data len:{}".format(len(data[1])))\r
+ # print("self.AI_client_VC = {} , self.AI_client_VC_isconnect = {}".format(self.AI_client_VC,self.AI_client_VC_isconnect)) \r
+ if self.AI_client_VC != None and self.AI_client_VC_isconnect == 1:\r
+ self.AI_client_VC.send(data[1])\r
+ else:\r
+ try:\r
+ if isinstance(data[1], str) and data[1] != "ping":\r
+ _ev, _tid = _vc_out_json_event_meta(data[1])\r
+ print("VC VC: send_data:ERROR len={} event={} trace_id={}".format(\r
+ len(data[1]), _ev or "?", _tid or "-"))\r
+ else:\r
+ print("VC VC: send_data:ERROR:{}".format(len(data[1])))\r
+ except Exception:\r
+ print("VC VC: send_data:ERROR:{}".format(len(data[1])))\r
+ #print("VC: send_data end")\r
+ \r
+ #print("VC: send_data end")\r
+ except Exception as e:\r
+ print("DeviceInfo_queue_thread:ERROR:{}".format(e))\r
+ usys.print_exception(e)\r
+ if data[0] == AI_client_VC_num:\r
+ #self.websocket_error()\r
+ pass\r
+\r
+ \r
+ try:\r
+ if self.AI_client_VC_isconnect != 0:\r
+ print("VC发送数据错误,退出")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC发送出错了,请重试"}) \r
+ self.ud_error(["VC",self.VC_taskid])\r
+ except Exception as e: #网络连接异常\r
+ usys.print_exception(e)\r
+\r
+ utime.sleep_ms(1)\r
+ self.AI_VC_send_data_queue_thread_lock = 0 \r
+ #VC关闭读线程\r
+ def AI_client_VC_recv_thread_close(self):\r
+ if self.AI_client_VC_recv_id != 0:\r
+ count =500\r
+ while True:\r
+ count -= 1\r
+ if self.AI_client_VC_recv_lock == 1:\r
+ self.AI_client_VC_recv_lock = 2\r
+ if self.AI_client_VC_recv_lock == 0 or count <=0:\r
+ break\r
+ else:\r
+ print("AI_client_VC_recv_thread going:{}...".format(count))\r
+ utime.sleep_ms(1)\r
+ self.AI_client_VC_recv_lock = 0\r
+ print("AI_client_VC_recv_thread_close")\r
+ if self.AI_client_VC_recv_id!= 0:\r
+ try:\r
+ _thread.stop_thread(self.AI_client_VC_recv_id)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.AI_client_VC_recv_id = 0\r
+ #Ai VC websocket返回数据 \r
+ def AI_client_VC_recv(self,cli):\r
+ count = 0\r
+ asr_led_flag = 0\r
+ tts_led_flag = 0\r
+ while True:\r
+ try:\r
+ # 死循环接收数据\r
+ self.AI_client_VC_recv_lock = 0\r
+\r
+ count+=1\r
+ if self._vc_tts_pipeline_busy() and (self.tts_in_data_len - self.tts_out_data_len > 64*1024 or count>5):\r
+ count = 0\r
+ utime.sleep_ms(1)\r
+ if not self._vc_tts_pipeline_busy():\r
+ utime.sleep_ms(1)\r
+ recv_data = cli.recv()\r
+ if self.AI_client_VC_recv_lock == 2:\r
+ print("AI_client_VC_recv 该退出了")\r
+ break\r
+ self.AI_client_VC_recv_lock = 1\r
+ if self._vc_rx_resets_long_idle_timer(recv_data):\r
+ self.close_timerout = 0\r
+ if recv_data == "websocket close":\r
+ # 服务器关闭连接\r
+ print("VC 服务器关闭连接!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ #self.AI_client_VC_isconnect = 0\r
+ #self.websocket_deconnect_VC()\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr}) \r
+ # #EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(1500)})\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_connect_asr}) \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC连接断开了,请重试"}) \r
+ self.ud_error(["VC",self.VC_taskid])\r
+ break\r
+ elif recv_data == "":#客户端关闭连接\r
+ print("VC VC :客户端关闭连接")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC连接断开了,请重试"}) \r
+ self.ud_error(["VC",self.VC_taskid])\r
+ break\r
+ try:\r
+ self.websocket_error_count = 0\r
+ if isinstance(recv_data, str):#返回的是字符串\r
+ jsload_data = ujson.loads(recv_data)\r
+ if VC_WS_IO_LOG:\r
+ print("[VC] recv raw:{}".format(recv_data))\r
+ else:\r
+ try:\r
+ _rev = jsload_data.get("event", "")\r
+ if _rev != "llm.delta":\r
+ print("[VC] rx event={} sid={}".format(\r
+ _rev, jsload_data.get("sid", "") or "-"))\r
+ except Exception:\r
+ pass\r
+ #print("AI_client_VC_recv jsload_data = {}".format(jsload_data))\r
+ try:\r
+ self._cur_sid = jsload_data.get("sid", "") or ""\r
+ except Exception:\r
+ self._cur_sid = ""\r
+ try:\r
+ ev = jsload_data.get("event", "")\r
+ if ev:\r
+ self._vc_dispatch_server_event(ev, jsload_data)\r
+ except Exception:\r
+ pass\r
+ \r
+ if jsload_data["event"] == "started" :#会话开始\r
+ #payload = jsload_data["payload"]\r
+ # self.VC_dialog_id = payload["dialog_id"] \r
+ # self.VC_task_id = jsload_data["task_id"] \r
+ # started 仅表示链路就绪:不在这里提前切灯态;\r
+ # 聆听灯只在“录音真正开始/停止”处更新(VC_Speech_Start/End)。\r
+ # 会话开始:允许上行;连续对话模式下直接进入“可发送音频”状态\r
+ self._vc_session_started = 1\r
+ # started 时不要强制清 Close_flag:\r
+ # - Close_flag==1:提示音/模式切换播完退出\r
+ # - Close_flag==0:正常对话流程\r
+ self._ptt_audio_created = 0\r
+ self._server_vad_silence = 0\r
+ self._turn_has_asr_completed = 0\r
+ self._turn_has_vad_silence = 0\r
+ self._turn_has_tts_started = 0\r
+ self._llm_completed_ms = 0\r
+ if self.VC_mode == 0 and getattr(self, "Close_flag", 0) == 0:\r
+ _skip_uplink = 0\r
+ try:\r
+ if getattr(self, "_skip_started_uplink_once", 0) == 1:\r
+ self._skip_started_uplink_once = 0\r
+ _skip_uplink = 1\r
+ print("[VC][uplink_ctrl] Deny START by server.started cause=boot_prompt")\r
+ except Exception:\r
+ pass\r
+ if not _skip_uplink:\r
+ # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后\r
+ # 出现首轮无音频上行导致“第一次对话没有”的情况。\r
+ if self.record_data_save_flag == 0:\r
+ if self.VC_record_state_flag == 0:\r
+ self.request_uplink_start("server.started")\r
+ else:\r
+ self._vc_clear_stale_record_state("server.started")\r
+ self.request_uplink_start("server.started/retry")\r
+ \r
+ print("VC:Started!!!!!!!!\r\n")\r
+ print("TTS音频为:{},语音下发地址为:{}".format(self.AI_TTS_str,self.AI_new_voice_url))\r
+ if self.AI_new_voice_url != "" and self.alarm_play_flag == 1 :#有闹钟,先响闹钟\r
+ self.AI_VC_start()\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放铃声\r
+ self.alarm_play_flag = 0\r
+ self.voice_choose_flag = 1\r
+ elif self.AI_TTS_str == "" and self.AI_new_voice_url != "" and self.music_play_flag == 0:\r
+ # if EventMesh.publish("get_history_voice_flag") == 1:\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ # self.recv_voice = 0#当留言列表为空,留言播放标志清空\r
+ # else:\r
+ # self.voice_cnt = 0\r
+ # print("定时器开启222222222")\r
+ # self.voice_timer.start(1000,1,self.voice_judge)\r
+ elif self.AI_TTS_str != "" and self.AI_new_voice_url == "":\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
+ \r
+ elif self.AI_TTS_str != "" and self.AI_new_voice_url != "" and self.music_play_flag == 0:#遇到tts和语音留言冲突时(并且没有音乐播放)\r
+ if EventMesh.publish("get_history_voice_flag") == 1:#播放历史留言\r
+ if EventMesh.publish("get_wait_voice_flag") == 0:\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
+ self.his_cnt = 0\r
+ self.his_timer.start(1000,1,self.history_play) \r
+ self.voice_choose_flag = 1\r
+ else:#等待留言\r
+ if self.pwk_notice_flag == 0:\r
+ self.wait_cnt = 0\r
+ print("未听播放1")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
+ self.wait_timer.start(1000,1,self.wait_play) \r
+ self.voice_choose_flag = 1\r
+ \r
+ else:#已确定播放\r
+ print("未听播放2")\r
+ self.wait_cnt = 0\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
+ self.wait_timer.start(1000,1,self.wait_single_play) \r
+ self.voice_choose_flag = 1\r
+ else:#播放正常留言\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
+ self.voice_cnt = 0\r
+ print("定时器开启111111111")\r
+ self.voice_timer.start(1000,1,self.voice_judge) \r
+ self.voice_choose_flag = 1\r
+ # self.AI_TTS_str = "" \r
+ elif self.AI_TTS_str != "" and self.AI_new_voice_url != "" and self.music_play_flag == 1 :#遇到tts和语音留言冲突时(并且有音乐播放)\r
+ self.music_play_flag = 0\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
+ self.music_timer.start(1000,1,self.music_play) \r
+ self.voice_choose_flag = 1\r
+ else:\r
+ #开始上传语音\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("UPLOAD_START_TONE_ENABLED", False):\r
+ self.chenggong_tone()\r
+ except Exception:\r
+ pass\r
+ if self.VC_mode == 0:\r
+ self.request_uplink_start("server.started/idle")\r
+ else:\r
+ # Push-To-Talk:必须先同步发出 audio.create,再开麦。\r
+ # request_send_audio_create() 走 EventMesh 异步队列,会导致录音线程先于\r
+ # audio.create 上行,深度睡眠唤醒后首轮 ASR 空结果、第二轮才正常。\r
+ try:\r
+ self.AI_client_VC_send_speech()\r
+ except Exception:\r
+ self.request_send_audio_create()\r
+ # 若长按发生在“未 started”窗口,收到 started 后立即开麦进入新一轮 PTT\r
+ try:\r
+ if getattr(self, "_ptt_force_record_on_started", 0) == 1:\r
+ self._ptt_force_record_on_started = 0\r
+ self.VC_Speech_Start_now()\r
+ except Exception:\r
+ pass\r
+ \r
+ \r
+ # if self.AI_new_voice_url != "":\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})\r
+\r
+ elif jsload_data["event"] == "config.update" :#配置更新中,需要停止录音\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ self.request_uplink_stop("config.update")\r
+\r
+ elif jsload_data["event"] == "asr.started" :#asr开始 \r
+ try:\r
+ self.fsm.on_server_asr_started()\r
+ except Exception:\r
+ pass\r
+ self._server_vad_silence = 0\r
+ self._turn_has_asr_completed = 0\r
+ self._turn_has_vad_silence = 0\r
+ self._turn_has_tts_started = 0\r
+ self._llm_completed_ms = 0\r
+ try:\r
+ self._asr_completed_tone_done = 0\r
+ except Exception:\r
+ pass\r
+ # 单对话:_drop_tts_audio 在 VC_Speech_Start 真开录后再清 0,避免 canceled 后仍播旧 TTS\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ self._drop_tts_audio = 0\r
+ except Exception:\r
+ pass\r
+ # 单对话:勿在 asr.started 置 flag=2(未真开麦会僵死);由 VC_Speech_Start 维护\r
+ self.LocalRespondingEnded_timerout = 0\r
+ elif jsload_data["event"] == "asr.result" :#asr结果\r
+ try:\r
+ self.AI_ASR_str = jsload_data["payload"]["content"]\r
+ except Exception:\r
+ try:\r
+ self.AI_ASR_str = jsload_data.get("payload", {}).get("text", "") or ""\r
+ except Exception:\r
+ self.AI_ASR_str = ""\r
+ print("[VC][ASR] {}".format(self.AI_ASR_str))\r
+ try:\r
+ if (self.AI_ASR_str or "").strip():\r
+ self._last_asr_result_text = self.AI_ASR_str\r
+ except Exception:\r
+ pass\r
+ if self.AI_ASR_str == "":#没识别到\r
+ self.AI_TTS_str = "我没听清,请再说一遍"\r
+ self._vc_led_alert_rb_flash()\r
+ elif jsload_data["event"] == "asr.completed" :#asr 终止的标识 \r
+ self._turn_has_asr_completed = 1\r
+ try:\r
+ if not (self.AI_ASR_str or "").strip():\r
+ self.AI_ASR_str = getattr(self, "_last_asr_result_text", "") or ""\r
+ except Exception:\r
+ pass\r
+ # 本地 think 提示:仅在有有效 ASR 文本时播,避免空轮次连播「滴」\r
+ try:\r
+ if getattr(self, "_asr_completed_tone_done", 0) == 1:\r
+ pass\r
+ elif not self._vc_effective_asr_text():\r
+ try:\r
+ print("[VC][ASR] completed empty, skip think tone")\r
+ except Exception:\r
+ pass\r
+ else:\r
+ stream_on = False\r
+ try:\r
+ stream_on = EventMesh.publish("get_vc_tts_playback_active") == 1\r
+ except Exception:\r
+ stream_on = False\r
+ pcm_busy = getattr(self, "play_pcm", None) is not None\r
+ playing = getattr(self, "VC_Play_flag", 0) == 1\r
+ try:\r
+ thinking = getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking\r
+ except Exception:\r
+ thinking = False\r
+ if (not stream_on) and (not pcm_busy) and (not playing) and (not thinking):\r
+ self._vc_local_play('U:/static/think.mp3', 1, 1, 650)\r
+ self._asr_completed_tone_done = 1\r
+ except Exception:\r
+ pass\r
+ print("self.commands_mode:{}".format(self.commands_mode))\r
+ if self.commands_mode ==2:\r
+ ret= self.check_text(self._vc_effective_asr_text())\r
+ if ret == 0:#配置指令\r
+ \r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ self.AI_ASR_send_buf_len = 0\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ try:\r
+ \r
+ self.request_uplink_stop("asr.completed/config_cmd")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ continue\r
+ elif ret == 1:#不处理\r
+ pass \r
+ #self.VC_Speech_End()\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+\r
+ # if self.record_data_save_flag == 1:\r
+ # if self.record_fd :\r
+ # self.record_fd.close()\r
+ # self.record_fd = None\r
+\r
+ # if dev_type != "7PIN" :\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.send_tone}) \r
+ elif jsload_data["event"] == "vad.active" :#VAD 检测到说话→聆听\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("VAD_ACTIVE_BLUE_LED", True):\r
+ self._server_vad_silence = 0\r
+ except Exception:\r
+ pass\r
+ elif jsload_data["event"] == "vad.silence" :#VAD 终止的标识 \r
+ # 说明服务端已判定“本轮说话结束”;此时继续上行只会干扰后续(例如推迟 TTS)。\r
+ # 仅在“正在录音上行”的语音阶段才停止,避免误触发。\r
+ self._server_vad_silence = 1\r
+ self._turn_has_vad_silence = 1\r
+ self._uplink_mark_end("vad.silence")\r
+ if self.record_data_save_flag == 0:\r
+ if self.VC_record_state_flag != 0 and self._vc_vad_uplink_stop_eligible():\r
+ self.request_uplink_stop("vad.silence")\r
+ elif jsload_data["event"] == "llm.created" :#思考(LLM)\r
+ try:\r
+ self._ptt_await_server = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ except Exception:\r
+ pass\r
+ self._llm_completed_ms = 0\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})\r
+ pass\r
+ elif jsload_data["event"] == "llm.completed" :#llm完成(可能无 tts)\r
+ try:\r
+ self._llm_completed_ms = utime.ticks_ms()\r
+ except Exception:\r
+ # 勿用 1:ticks_diff(now,1)≈开机至今,会瞬间满足 thinking 超时误以为该重启\r
+ self._llm_completed_ms = 0\r
+ try:\r
+ self._cur_dialog_id = jsload_data.get("payload", {}).get("dialog_id", "") or ""\r
+ except Exception:\r
+ self._cur_dialog_id = ""\r
+ try:\r
+ self.fsm.log("server.llm.completed")\r
+ except Exception:\r
+ pass\r
+ pass\r
+ elif jsload_data["event"] == "llm.delta" :#llm返回数据\r
+ if self.is_VAD == 0 and self.record_data_save_flag == 0:\r
+ if self.VC_record_state_flag == 1:#llm返回数据还没停止,就先停止\r
+ self.request_uplink_stop("llm.delta")\r
+ elif jsload_data["event"] == "tts.started" :#tts开始\r
+ try:\r
+ self._ptt_await_server = 0\r
+ except Exception:\r
+ pass\r
+ self._server_vad_silence = 1\r
+ self._turn_has_tts_started = 1\r
+ self._tts_pending = 0\r
+ self._tts_pending_ms = 0\r
+ self._uplink_mark_end("tts.started")\r
+ # stop uplink + play_start 由 FSM action list 统一触发,避免多处重复调用\r
+ # 标记本轮 TTS 来源:\r
+ # - 有 payload.dialog_id:认为是“对话回复 TTS”\r
+ # - 无 dialog_id:认为是“提示/提醒 TTS”(模式切换可通过 _pending_tts_kind 标记)\r
+ try:\r
+ did = jsload_data.get("payload", {}).get("dialog_id", "") or ""\r
+ except Exception:\r
+ did = ""\r
+ # 注意:无 dialog_id 的 tts.started 很可能是提示音/提醒音;\r
+ # 但“模式切换提示音”的 kind 可能已在 synthesis 时写入 _last_tts_kind,\r
+ # 这里不应无条件覆盖为 prompt,否则播完无法触发下一轮开麦。\r
+ try:\r
+ if did:\r
+ self._last_tts_kind = "dialog"\r
+ else:\r
+ k = getattr(self, "_pending_tts_kind", "") or ""\r
+ if k:\r
+ self._last_tts_kind = k\r
+ else:\r
+ cur = getattr(self, "_last_tts_kind", "") or ""\r
+ if cur in (\r
+ "mode_switch_long",\r
+ "mode_switch_ptt",\r
+ "boot_prompt",\r
+ ):\r
+ pass\r
+ elif not cur:\r
+ self._last_tts_kind = "prompt"\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._pending_tts_kind = ""\r
+ except Exception:\r
+ pass\r
+ # 播放灯由阶段事件控制\r
+ elif jsload_data["event"] == "tts.completed" :#tts结束\r
+ # tts.completed 仅表示下发结束,本地可能仍在播放;绿灯关闭在本地播放结束时处理\r
+ try:\r
+ # server event 已在统一入口 on_server_event 触发 FSM,这里避免重复驱动\r
+ pass\r
+ except Exception:\r
+ pass\r
+ try:\r
+ try:\r
+ sid = getattr(self, "_cur_sid", "")\r
+ did = getattr(self, "_cur_dialog_id", "")\r
+ except Exception:\r
+ sid, did = "", ""\r
+ print("[TTS] completed in_len={} out_len={} q_empty={} play_pcm={} sid={} did={}".format(\r
+ getattr(self, "tts_in_data_len", 0),\r
+ getattr(self, "tts_out_data_len", 0),\r
+ 1 if self.AI_audio_queue.empty() else 0,\r
+ 1 if self.play_pcm != None else 0,\r
+ sid,\r
+ did,\r
+ ))\r
+ except Exception:\r
+ pass\r
+ # 播完上报完全由 FSM await_played + play_end 驱动,这里不再使用 LocalRespondingEnded_timerout/_need_audio_played_ack\r
+ pass\r
+\r
+ elif jsload_data["event"] == "canceled" :#打断\r
+ # 停播/清队列已在 _vc_dispatch_server_event(canceled) 完成;此处仅收尾标志\r
+ self.is_ruppt_tts = 0#设备上发中断tts,收到服务器返回的cancel后置为0\r
+ self._need_audio_played_ack = 0\r
+ self._tts_pending = 0\r
+ self._tts_pending_ms = 0\r
+ # 开麦由 FSM action list 统一触发(uplink_start:canceled),避免重复调用\r
+ if self.AI_audio_queue_thread_id ==0:\r
+ _thread.stack_size(24*1024)\r
+ self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())\r
+ _thread.stack_size(8*1024)\r
+ print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))\r
+ else:\r
+ print("AI_VC_start :AI_audio_queue_thread_id != 0 ")\r
+ elif jsload_data["event"] == "completed" :#会话结束(可能无 TTS,如 ASR 空)\r
+ # 重要:服务端常在 tts.completed 后紧跟 completed。\r
+ # 若本轮存在 TTS/待发 audio.played,则不能在此清零 LocalRespondingEnded_timerout/_need_audio_played_ack,\r
+ # 否则会掐断「播完→audio.played→下一轮录音」链路,表现为最多 1~2 轮就停。\r
+ if self._need_audio_played_ack == 1 or self._vc_tts_pipeline_busy() or self.VC_Play_flag == 1:\r
+ pass\r
+ else:\r
+ # 无 TTS 的 completed:清状态并按需进入下一轮(见下方条件)\r
+ self.LocalRespondingEnded_timerout = 0\r
+ self._need_audio_played_ack = 0\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ # 连续对话:若本轮没有 TTS(或服务端直接 completed),需要立刻回到聆听状态\r
+ # 否则会出现“有时 1 轮/2 轮后就停住”的现象(服务端 completed 但端侧未重新开录音上行)。\r
+ if self.VC_mode == 0 and self.Close_flag == 0:\r
+ # 仅当“本轮已结束一次语句(vad.silence 或 asr.completed)且从未进入 tts.started”\r
+ # 才自动进入下一轮;避免把真正的“会话结束/异常结束”误当成下一轮入口。\r
+ if (self._turn_has_tts_started == 0) and (self._turn_has_vad_silence == 1 or self._turn_has_asr_completed == 1):\r
+ self._server_vad_silence = 0\r
+ # 开麦由 FSM action list 统一触发(uplink_start:completed),这里不再重复调用\r
+ pass\r
+\r
+ elif jsload_data["event"] == "command" :#命令匹配\r
+ ret = -1\r
+ try:\r
+ if self.commands_mode ==1:\r
+ if len(jsload_data["payload"]["functions"])>0:\r
+ \r
+ cmd = jsload_data["payload"]["functions"][0]["function"]["arguments"]\r
+ print("llm:cmd:{}".format(cmd))\r
+ cmd = ujson.loads(cmd)\r
+ ret = self.check_text_llm(cmd)\r
+ elif self.commands_mode ==0:\r
+\r
+ cmd = jsload_data["payload"]\r
+ print("server:cmd:{}".format(cmd))\r
+ ret = self.check_text_server(cmd)\r
+ else:\r
+ cmd = jsload_data["payload"]["functions"][0]["function"]["arguments"]\r
+ print("llm:cmd:{}".format(cmd))\r
+ cmd = ujson.loads(cmd)\r
+ ret = self.check_text_llm(cmd)\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ if ret == -1:\r
+ if self.AI_TTS_str =="":\r
+ self.AI_TTS_str = "抱歉,没找到对应操作,可以换个说法试试" \r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})\r
+ self.request_uplink_stop("command")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ elif jsload_data["event"] == "failed":#接口返回错误\r
+ if jsload_data["payload"]["error"] == "concurrency_limit_exceeded":\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.concurrency_limit_exceeded_tone}) \r
+ \r
+ #utime.sleep_ms(4000)\r
+ \r
+ else:\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":jsload_data["payload"]["message"]}) \r
+ pass\r
+ \r
+ elif jsload_data["event"] == "audio.convert.completed":#音频转成完成没有出错 接口 \r
+ # self.VC_state = VC_State_End\r
+ print("未读留言初始数值:{}".format(self.pwk_cnt_voice))\r
+ if self.pwk_notice_flag == 1:#只有在播放的时候才会去计数\r
+ if self.pwk_cnt_voice > 0:\r
+ self.pwk_cnt_voice -= 1#减1\r
+ print("未读留言又-1,当前留言剩余数量为:{}".format(self.pwk_cnt_voice))\r
+ else:\r
+ print("未读列表为空")\r
+ if self.pwk_cnt_voice == 0:#积累的留言已经全部播完\r
+ self.pwk_notice_flag = 0\r
+ self.wait_sort = 0\r
+ self.wait_first_flag = 0#让下次未读播放提示恢复\r
+ self.voicerupt_flag = 0 #标志位置0\r
+ self.wait_voicerupt_flag = 0\r
+ self.voice_notice = 1#开启回复\r
+ if self.history_voice_flag == 1 and self.wait_voice_flag == 0:\r
+ self.voice_notice = 0\r
+ EventMesh.publish("set_storage_config",["is_noread_voice",0])\r
+ \r
+ if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭\r
+ self.voice_choose_flag = 0\r
+ self.Close_flag = 1\r
+ self.LocalRespondingEnded_timerout = 8 \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ if self.voice_notice == 1:#已收到留言,进行该留言回复逻辑处理\r
+ tts_play_str = "如需回复,长按确认键开始录制,松开发送。十秒内无操作自动退出"\r
+ EventMesh.publish("play_tts",[tts_play_str,1])\r
+ self.voice_notice = 0#标志位清空\r
+ self.voice_timer.stop()\r
+ self.revoice_cnt = 0\r
+ self.voice_flag = 1\r
+ print("语音留言回复停止")\r
+ self.revoice_timer.start(1000,1,self.revoice_judge)\r
+ print("是否未读:{},是否播完:{}".format(EventMesh.publish("get_wait_voice_flag"),len(EventMesh.publish("get_play_new_voice_list"))))\r
+ # if EventMesh.publish("get_wait_voice_flag") == 1 and len(EventMesh.publish("get_play_new_voice_list")) ==0:#还在未读并且列表为空\r
+ # self.wait_first_flag = 0\r
+ # print("未读留言播完,清空标志")\r
+ # EventMesh.publish("set_wait_voice_flag",0)\r
+ # EventMesh.publish("set_history_voice_flag",0)\r
+ EventMesh.publish("update_play_new_voice_info")#更新已读状态\r
+ self.recv_voice = 0\r
+ print("不开启定时器标志位置0")\r
+ self.LocalRespondingEnded_timerout = 8\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ \r
+ pass\r
+ elif jsload_data["event"] == "audio.convert.failed": #音频转换失败了\r
+ pass\r
+ \r
+ \r
+ else:#下发音频数据\r
+ #self.AI_ASR_str = ""#下发音频后清掉asr结果\r
+ \r
+ \r
+ # PTT 打断后丢弃残留 TTS 音频帧,避免“断断续续继续播”\r
+ try:\r
+ if self._ptt_should_drop_inbound_tts():\r
+ continue\r
+ except Exception:\r
+ pass\r
+ if self.VC_stop_play_audio_flag == 0:\r
+ print("新的AI对话不允许以前的播放数据:")\r
+ break\r
+\r
+ # if VC_AUDIO_FRAME_LOG:\r
+ # print("下发音频数据1:{}".format(len(recv_data)))\r
+\r
+ if self.play_data_save_flag == 1:\r
+ if self.play_fd ==None:\r
+ try:\r
+ uos.remove('/usr/play_data.pcm')\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.play_fd = open('/usr/play_data.pcm','w')\r
+ if self.play_fd !=None:\r
+ self.play_fd.write(recv_data)\r
+ # print("下发音频数据2:{}".format(len(recv_data)))\r
+ #if self.VC_state >= VC_State_TTSStart:\r
+ #self.put_record_data([recv_data,""])\r
+ if self.is_ruppt_tts == 0:#被设备主动打断打断则不播放,知道收到服务器返回的cancel才修改标志位\r
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
+ if self.play_pcm != None:\r
+ try:\r
+ #print("play_pcm.stream_write !!!")\r
+ buf_len = self.play_pcm.stream_get_buf_len()\r
+ count_tmp = 1000\r
+ while buf_len < len(recv_data):\r
+ utime.sleep_ms(1)\r
+ count_tmp-=1\r
+ if count_tmp<=0 or self.play_pcm == None:\r
+ break\r
+ buf_len = self.play_pcm.stream_get_buf_len()\r
+ #print("buf已满,等待释放:buf_len:{},count_tmp:{}".format(buf_len,count_tmp))\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.put_record_data,"data":[recv_data,""]}) \r
+ else:\r
+ print("被打断不播放") \r
+ utime.sleep_ms(1)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ \r
+ except Exception as e:\r
+ error_str = str(e)\r
+ #print("VC VC:e:{}".format(error_str))\r
+ if error_str.find("[Errno 110] ETIMEDOUT")!= -1 or error_str.find("[Errno 115] ")!= -1:\r
+ if VC_HEARTBEAT_LOG:\r
+ print("VC VC:recv 正常报错:{}".format(error_str))\r
+ # recv 超时仅表示本周期无帧,链路仍可能在等 LLM/TTS;清零避免误触发「接口无响应」\r
+ self.close_timerout = 0\r
+ pass\r
+ else:\r
+ usys.print_exception(e)\r
+ #self.websocket_error()\r
+ print("VC VC :读数据报错,退出")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC出错了,请重试"}) \r
+ self.ud_error(["VC",self.VC_taskid])\r
+ break\r
+ self.AI_client_VC_isconnect = 0\r
+ self.AI_client_VC_recv_id = 0 \r
+ self.AI_client_VC_recv_lock = 0\r
+ print("[VC] recv_thread exit")\r
+ \r
+ def voice_play_10S(self,event,msg):#按键开启留言播放等待10S定时器判断\r
+ self.voice_timer.start(1000,1,self.voice_judge)\r
+ #语音留言定时器\r
+ def voice_judge(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.voice_judger_cb}) \r
+ def voice_judger_cb(self):\r
+ self.voice_cnt += 1\r
+ # print("语音留言定时器cnt = {}".format(self.voice_cnt))\r
+ if self.voicerupt_flag == 1:#按键被按下\r
+ self.pwk_notice_flag = 1#表示进入收听未听留言\r
+ self.AI_VC_start()\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ self.voice_cnt = 0\r
+ # self.recv_voice = 0#进入留言定时器,留言清空\r
+ print("进入留言定时器1,留言清空!!!!!")\r
+ EventMesh.publish("update_play_new_voice_info")#更新已读状态\r
+ self.voice_timer.stop()\r
+ if self.pwk_cnt_voice > 0:\r
+ EventMesh.publish("get_new_voice_msg_S")\r
+ if self.voice_cnt > 15:#10S后退出播放,5S是为了给语音提示留时间\r
+ self.voice_cnt = 0\r
+ self.recv_voice = 0#进入留言定时器,留言清空\r
+ if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭\r
+ self.voice_choose_flag = 0\r
+ print("进入留言定时器2,留言清空!!!!!")\r
+ self.voice_timer.stop()\r
+ #语音留言回复定时器\r
+ def revoice_judge(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.revoice_judger_cb}) \r
+ def revoice_judger_cb(self):\r
+ self.revoice_cnt += 1\r
+ # print("语音留言回复定时器cnt = {}".format(self.revoice_cnt))\r
+ if self.revoice_cnt > 20:#10S后退出播放,10S是为了给语音提示留时间\r
+ self.revoice_cnt = 0\r
+ self.voice_flag=0\r
+ EventMesh.publish("set_1M_nowplay_flag",0)\r
+ print("语音留言回复停止")\r
+ self.revoice_timer.stop()\r
+ #音乐播放定时器\r
+ def music_play(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.music_play_cb}) \r
+ def music_play_cb(self):\r
+ self.music_cnt += 1\r
+ if self.music_cnt > 4:#4S后播放音乐,4S是为了给语音提示留时间\r
+ self.AI_VC_start()\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ self.music_cnt = 0\r
+ self.music_timer.stop() \r
+ #历史留言播放定时器\r
+ def history_play(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.history_play_cb}) \r
+ def history_play_cb(self):\r
+ self.his_cnt += 1\r
+ if self.his_cnt > 4:#4S后播放,4S是为了给语音提示留时间\r
+ self.AI_VC_start()\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ self.his_cnt = 0\r
+ self.his_timer.stop() \r
+ #未读留言是否播放定时器\r
+ def wait_play(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_play_cb}) \r
+ def wait_play_cb(self):\r
+ self.wait_cnt += 1\r
+ if self.wait_voicerupt_flag == 1:#按键被按下\r
+ EventMesh.publish("set_is_listen_wait",1)#已听\r
+ self.pwk_notice_flag = 1#表示进入收听未听留言\r
+ self.AI_VC_start()\r
+ self.wait_sort = 1\r
+ self.AI_TTS_str = "第"+str(self.wait_sort)+"条" \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ # self.wait_cnt = 0\r
+ # print("进入留言定时器1,留言清空!!!!!")\r
+ # EventMesh.publish("update_play_new_voice_info")#更新已读状态\r
+ self.wait_timer.stop()\r
+ if self.wait_cnt > 10:#10S后退出播放,5S是为了给语音提示留时间\r
+ self.wait_cnt = 0\r
+ self.recv_voice = 0#进入留言定时器,留言清空\r
+ EventMesh.publish("set_history_voice_flag",0)\r
+ EventMesh.publish("set_wait_voice_flag",0)\r
+ EventMesh.publish("set_open_device",0)#开机不听\r
+ self.wait_first_flag = 0\r
+ if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭\r
+ self.voice_choose_flag = 0\r
+ print("进入留言定时器2,留言清空!!!!!")\r
+ self.wait_timer.stop()\r
+ #未读留言一条条播放定时器\r
+ def wait_single_play(self, *args, **kwargs):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_single_play_cb}) \r
+ def wait_single_play_cb(self):\r
+ self.wait_cnt += 1\r
+ if self.wait_cnt > 2:#2S后播放,2S是为了给语音提示留时间\r
+ self.AI_VC_start()\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ self.wait_cnt = 0\r
+ self.wait_timer.stop() \r
+ def set_voicerupt_flag(self,event,msg):#按键控制当前留言播放\r
+ self.voicerupt_flag = msg\r
+ def set_wait_voicerupt_flag(self,event,msg):#按键控制未读留言播放\r
+ self.wait_voicerupt_flag = msg \r
+ def get_voicerupt_flag(self,event,msg):#获取按键控制留言播放状态\r
+ return self.voicerupt_flag\r
+ def get_recv_voice(self,event,msg):\r
+ return self.recv_voice\r
+ def set_recv_voice(self,event,msg):\r
+ return self.recv_voice\r
+ def put_record_data(self,data):\r
+ try:\r
+ if self._ptt_should_drop_inbound_tts():\r
+ return\r
+ except Exception:\r
+ pass\r
+ #print("下发音频数据1:{}".format(len(data[0])))\r
+ ret=self.AI_audio_queue.put(data)\r
+ try:\r
+ if self.tts_in_data_len == 0 and data and len(data[0]) > 0:\r
+ try:\r
+ sid = getattr(self, "_cur_sid", "")\r
+ did = getattr(self, "_cur_dialog_id", "")\r
+ except Exception:\r
+ sid, did = "", ""\r
+ print("[TTS] first_chunk len={} play_pcm={} VC_Play_flag={} sid={} did={}".format(\r
+ len(data[0]),\r
+ 1 if self.play_pcm != None else 0,\r
+ getattr(self, "VC_Play_flag", 0),\r
+ sid,\r
+ did,\r
+ ))\r
+ except Exception:\r
+ pass\r
+ self.tts_in_data_len += len(data[0])\r
+ #print("下发音频数据2:{},{}".format(len(data[0]),self.tts_in_data_len))\r
+ #msg[0]是tts播放字符串,msg[1]是是否关闭当前tts\r
+ def play_tts(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_tts_cb,"data":msg}) \r
+ \r
+ def play_tts_cb(self,data):\r
+ try:\r
+ tts_str = data[0]\r
+ is_close_VC = data[1]\r
+ if tts_str != "":\r
+ self.AI_TTS_str = tts_str\r
+ if is_close_VC != None:\r
+ self.Close_flag = is_close_VC\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ try:\r
+ if is_close_VC:\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
+ else:\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ except Exception:\r
+ pass\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_tts_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ pass\r
+ def check_text_server(self,cmd):\r
+ return -1\r
+\r
+ def _vc_cmd_diag(self, tag, text):\r
+ """本地指令诊断日志(保留,便于确认表驱动匹配情况)。"""\r
+ try:\r
+ print("[VC][cmd] {} type={} len={} repr={}".format(\r
+ tag, type(text).__name__, len(text) if text else 0, repr(text)[:72]))\r
+ if vc_text_intent is None:\r
+ print("[VC][cmd] {} vc_text_intent import=None".format(tag))\r
+ return\r
+ nt = vc_text_intent.normalize_vc_command_text(text)\r
+ print("[VC][cmd] {} norm_len={} norm_repr={}".format(tag, len(nt), repr(nt)[:72]))\r
+ if nt:\r
+ print("[VC][cmd] {} find 查询={} 信号={} 版本={}".format(\r
+ tag, nt.find("查询"), nt.find("信号"), nt.find("版本")))\r
+ for rule in vc_text_intent._INTENT_RULES:\r
+ if rule.get("id") == "query_signal":\r
+ subs = rule.get("all", ())\r
+ if subs:\r
+ kw = subs[0]\r
+ print("[VC][cmd] {} rule_kw0 repr={} kw_in_norm={}".format(\r
+ tag, repr(kw), nt.find(kw)))\r
+ break\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+\r
+ def _vc_intent_selftest_once(self):\r
+ if getattr(self, "_vc_intent_selftest_done", 0):\r
+ return\r
+ self._vc_intent_selftest_done = 1\r
+ try:\r
+ print("[VC][cmd] selftest start")\r
+ if vc_text_intent is None:\r
+ print("[VC][cmd] selftest FAIL vc_text_intent=None")\r
+ return\r
+ sample = "查询信号"\r
+ r = vc_text_intent.match_vc_command(sample, {})\r
+ print("[VC][cmd] selftest sample={} match={}".format(repr(sample), r))\r
+ self._vc_cmd_diag("selftest", sample)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+\r
+ def _apply_vc_text_intent(self, text, cmd, llm_style_tts):\r
+ """表驱动文本意图(vc_text_intent)。未命中返回 None;命中返回 0/1/-1。"""\r
+ if vc_text_intent is None:\r
+ print("[VC][cmd] vc_text_intent is None")\r
+ return None\r
+ try:\r
+ try:\r
+ _btv = bool("vc_config" in globals() and vc_config and vc_config.get_bool("BT_VOICE_FEATURES_ENABLED", False))\r
+ except Exception:\r
+ _btv = False\r
+ r = vc_text_intent.match_vc_command(text, {"bt_voice": _btv})\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ return None\r
+ if not r:\r
+ print("[VC][cmd] match_vc_command r is None")\r
+ self._vc_cmd_diag("match_fail", text)\r
+ return None\r
+ print("[VC][cmd] match intent={}".format(r.get("intent")))\r
+ return self._apply_vc_text_intent_matched(r, cmd, llm_style_tts)\r
+\r
+ def _apply_vc_text_intent_matched(self, r, cmd, llm_style_tts):\r
+ if r.get("needs_payload") and cmd is None:\r
+ print("[VC][cmd] needs_payload skip intent={}".format(r.get("intent")))\r
+ return None\r
+ it = r["intent"]\r
+ if r.get("tts"):\r
+ self.AI_TTS_str = r["tts"]\r
+ if r.get("close_flag"):\r
+ self.Close_flag = 1\r
+ if it == "goodbye":\r
+ return 0\r
+ if it == "shutdown":\r
+ if llm_style_tts:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
+ EventMesh.publish("Power_off")\r
+ return 1\r
+ try:\r
+ close_only = ("vc_config" in globals() and vc_config and vc_config.get_bool("POWEROFF_CMD_CLOSE_ONLY", True))\r
+ except Exception:\r
+ close_only = True\r
+ if close_only:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
+ return 1\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
+ EventMesh.publish("Power_off")\r
+ return 1\r
+ if it == "toggle_dialog_mode":\r
+ if self.VC_mode == 0:\r
+ self.VC_mode = 1\r
+ if llm_style_tts:\r
+ self.AI_TTS_str = "了解,已切换到单对话模式"\r
+ else:\r
+ try:\r
+ self.AI_TTS_str = vc_config.get_attr("CMD_TOGGLE_MODE_TO_PTT_TTS", "已切换到单对话模式")\r
+ except Exception:\r
+ self.AI_TTS_str = "已切换到单对话模式"\r
+ else:\r
+ self.VC_mode = 0\r
+ if llm_style_tts:\r
+ self.AI_TTS_str = "了解,已切换到长对话模式"\r
+ else:\r
+ try:\r
+ self.AI_TTS_str = vc_config.get_attr("CMD_TOGGLE_MODE_TO_LONG_TTS", "已切换到长对话模式")\r
+ except Exception:\r
+ self.AI_TTS_str = "已切换到长对话模式"\r
+ EventMesh.publish("set_storage_config", ["VC_mode", self.VC_mode])\r
+ return 0\r
+ if it == "volume_set":\r
+ try:\r
+ pj = ujson.loads(cmd["command"]["params_json"])\r
+ volume = pj["n"]\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try:\r
+ self.AI_TTS_str = cmd["command"]["fail_prompt"]\r
+ except Exception:\r
+ self.AI_TTS_str = ""\r
+ return -1\r
+ if volume > 100 or volume < 0:\r
+ self.AI_TTS_str = "音量调整超出范围了,只能0-100哦"\r
+ else:\r
+ self.VC_TTS_volume = volume\r
+ self.aud.setVolume(self.VC_TTS_volume // 10)\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_volume", self.VC_TTS_volume])\r
+ self.AI_TTS_str = "了解,音量已经调整,当前音量是{}".format(self.VC_TTS_volume)\r
+ EventMesh.publish("post_device_state")\r
+ EventMesh.publish("adjust_volume_toBle", self.VC_TTS_volume)\r
+ return 0\r
+ if it == "volume_up":\r
+ if llm_style_tts:\r
+ try:\r
+ print("self.VC_TTS_volume add:{}".format(self.VC_TTS_volume))\r
+ except Exception:\r
+ pass\r
+ self.VC_TTS_volume += 10\r
+ if self.VC_TTS_volume > 100:\r
+ self.AI_TTS_str = "音量已经是最大啦" if llm_style_tts else "音量已最大"\r
+ self.VC_TTS_volume = 100\r
+ else:\r
+ self.aud.setVolume(self.VC_TTS_volume // 10)\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_volume", self.VC_TTS_volume])\r
+ EventMesh.publish("post_device_state")\r
+ EventMesh.publish("adjust_volume_toBle", self.VC_TTS_volume)\r
+ self.AI_TTS_str = (\r
+ "了解,音量已经调大了,当前音量是{}".format(self.VC_TTS_volume)\r
+ if llm_style_tts\r
+ else "配置成功,当前音量 {}".format(self.VC_TTS_volume)\r
+ )\r
+ return 0\r
+ if it == "volume_down":\r
+ if llm_style_tts:\r
+ try:\r
+ print("self.VC_TTS_volume end:{}".format(self.VC_TTS_volume))\r
+ except Exception:\r
+ pass\r
+ self.VC_TTS_volume -= 10\r
+ if self.VC_TTS_volume < 0:\r
+ self.VC_TTS_volume = 0\r
+ self.AI_TTS_str = "音量已经是最小啦" if llm_style_tts else "音量已最小"\r
+ else:\r
+ self.aud.setVolume(self.VC_TTS_volume // 10)\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_volume", self.VC_TTS_volume])\r
+ EventMesh.publish("post_device_state")\r
+ EventMesh.publish("adjust_volume_toBle", self.VC_TTS_volume)\r
+ self.AI_TTS_str = (\r
+ "好嘞,音量已经调小了,当前音量是{}".format(self.VC_TTS_volume)\r
+ if llm_style_tts\r
+ else "配置成功,当前音量 {}".format(self.VC_TTS_volume)\r
+ )\r
+ return 0\r
+ if it in ("speech_faster", "speech_rate_up"):\r
+ self.speech_rate += 10\r
+ if self.speech_rate > 50:\r
+ self.AI_TTS_str = "语速已经是最快了"\r
+ self.speech_rate = 50\r
+ else:\r
+ EventMesh.publish("set_storage_config", ["speech_rate", self.speech_rate])\r
+ self.AI_TTS_str = "明白,语速已经调快了,当前语速是{}".format(self.speech_rate)\r
+ return 0\r
+ if it in ("speech_slower", "speech_rate_down"):\r
+ self.speech_rate -= 10\r
+ if self.speech_rate < -50:\r
+ self.speech_rate = -50\r
+ self.AI_TTS_str = "语速已经是最慢了"\r
+ else:\r
+ EventMesh.publish("set_storage_config", ["speech_rate", self.speech_rate])\r
+ self.AI_TTS_str = "明白,语速已经调慢了,当前语速是{}".format(self.speech_rate)\r
+ return 0\r
+ if it == "vad_faster":\r
+ self.vad_silence -= 200\r
+ if self.vad_silence < 100:\r
+ self.AI_TTS_str = "已经是最快了"\r
+ self.vad_silence = 100\r
+ else:\r
+ self.AI_TTS_str = "配置成功,当前语音断句速度是{}毫秒".format(self.vad_silence)\r
+ EventMesh.publish("set_storage_config", ["vad_silence", self.vad_silence])\r
+ return 0\r
+ if it == "vad_slower":\r
+ self.vad_silence += 200\r
+ if self.vad_silence > 2000:\r
+ self.vad_silence = 2000\r
+ self.AI_TTS_str = "已经是最慢了"\r
+ else:\r
+ self.AI_TTS_str = "配置成功,当前语音断句速度是{}毫秒".format(self.vad_silence)\r
+ EventMesh.publish("set_storage_config", ["vad_silence", self.vad_silence])\r
+ return 0\r
+ if it == "query_signal":\r
+ signal = net.csqQueryPoll()\r
+ self.AI_TTS_str = "当前信号{}".format(signal)\r
+ return 0\r
+ if it == "query_battery":\r
+ battery = EventMesh.publish("get_battery")\r
+ try:\r
+ print("电量查询:{}".format(battery))\r
+ except Exception:\r
+ pass\r
+ self.AI_TTS_str = "电量剩余百分之{}".format(battery)\r
+ return 0\r
+ if it == "query_voltage":\r
+ Vbatt = Power.getVbatt()\r
+ self.AI_TTS_str = "当前电池电压{}".format(Vbatt)\r
+ return 0\r
+ if it == "query_iccid":\r
+ iccid = sim.getIccid()\r
+ if iccid != -1 and len(iccid) > 5:\r
+ try:\r
+ print("iccid{}".format(iccid))\r
+ except Exception:\r
+ pass\r
+ self.AI_TTS_str = "当前卡号为:{}".format(iccid)\r
+ else:\r
+ self.AI_TTS_str = "卡号查询失败"\r
+ return 0\r
+ if it == "query_imei":\r
+ imei = modem.getDevImei()\r
+ if imei != -1 and len(imei) > 5:\r
+ try:\r
+ print("IMEI:{}".format(imei))\r
+ except Exception:\r
+ pass\r
+ self.AI_TTS_str = "当前设备号为:{}".format(imei)\r
+ else:\r
+ self.AI_TTS_str = "设备号查询失败"\r
+ return 0\r
+ if it == "query_version":\r
+ bt_versions = EventMesh.publish("get_bt_versions")\r
+ if bt_versions is None or bt_versions == "":\r
+ self.AI_TTS_str = "当前版本号:" + version_util.get_tts_version()\r
+ else:\r
+ self.AI_TTS_str = "当前版本号:" + version_util.get_tts_version(bt_versions)\r
+ return 0\r
+ if it == "query_server":\r
+ self.AI_TTS_str = "服务器地址" + self.server_ip\r
+ return 0\r
+ if it == "ota_check":\r
+ if self.ai_OTA.min_sys_ota() == 1:\r
+ self.AI_TTS_str = "有新版本,准备开始升级,升级过程会多次重启设备,请保证流量充足及电量充足"\r
+ else:\r
+ self.AI_TTS_str = "当前已是最新版本"\r
+ return 0\r
+ if it == "factory_mode":\r
+ self.factory_mode()\r
+ return 1\r
+ if it == "alarm_set":\r
+ try:\r
+ alarm = ujson.loads(cmd["command"]["params_json"])\r
+ try:\r
+ print("params_json:{}".format(cmd["command"]["params_json"]))\r
+ except Exception:\r
+ pass\r
+ add_alarm_tmp = EventMesh.publish("AI_add_alarm", alarm["alarm"])\r
+ if add_alarm_tmp[0] > 0:\r
+ self.AI_TTS_str = add_alarm_tmp[1]\r
+ return 1\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try:\r
+ self.AI_TTS_str = cmd["command"]["fail_prompt"]\r
+ except Exception:\r
+ self.AI_TTS_str = ""\r
+ return -1\r
+ if it == "alarm_clear":\r
+ try:\r
+ self.AI_TTS_str = EventMesh.publish("AI_delete_alarm")\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ return 0\r
+ if it == "alarm_list":\r
+ EventMesh.publish("read_alarm_list")\r
+ return 0\r
+ if it == "voice_message_mode":\r
+ self.voice_flag = 1\r
+ self.AI_TTS_str = "已进入语音留言模式,长按按钮开始录音,松开按钮结束"\r
+ self.Close_flag = 1\r
+ self.stop_voice_timer.stop()\r
+ self.stop_voice_timer.start(30 * 1000, 0, self.exit_voice_message_timer)\r
+ try:\r
+ print("语音留言!!!!!!!!!!")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ if it == "bt_ble_adv_on":\r
+ self.AI_TTS_str = "正在开蓝牙广播"\r
+ EventMesh.publish("AI_send_bt_ble_switch_toble", 1)\r
+ try:\r
+ print("正在开蓝牙广播")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ if it == "bt_ble_adv_off":\r
+ self.AI_TTS_str = "正在关蓝牙广播"\r
+ EventMesh.publish("AI_send_bt_ble_switch_toble", 2)\r
+ try:\r
+ print("正在关蓝牙广播")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ if it == "bt_classic_on":\r
+ self.AI_TTS_str = "正在开经典蓝牙"\r
+ EventMesh.publish("AI_send_bt_ble_switch_toble", 3)\r
+ try:\r
+ print("正在开经典蓝牙")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ if it == "bt_classic_off":\r
+ self.AI_TTS_str = "正在关经典蓝牙"\r
+ EventMesh.publish("AI_send_bt_ble_switch_toble", 4)\r
+ try:\r
+ print("正在关经典蓝牙")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ if it == "unbind_confirm":\r
+ self.AI_TTS_str = "设备解绑中...."\r
+ self.Close_flag = 1\r
+ EventMesh.publish("send_unbind_dev_to_iot")\r
+ try:\r
+ print("解绑设备!!!!!!!!!!!!!!")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ if it == "clear_earphone":\r
+ self.AI_TTS_str = "正在删除"\r
+ EventMesh.publish("clear_earphone_record")\r
+ try:\r
+ print("正在删除")\r
+ except Exception:\r
+ pass\r
+ return 0\r
+ return None\r
+\r
+ def check_text_llm(self,cmd):\r
+ try:\r
+ desc = cmd["command"]["chinese_description"]\r
+ if desc.find("切换音色") != -1:\r
+ text = cmd["command"]["params_json"]\r
+ try:\r
+ for key in self.VC_TTS_voice_type:\r
+ if text.find(key) != -1:\r
+ self.VC_TTS_voice_key = key\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ if text.find("男") != -1:\r
+ if "男" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "男"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ if text.find("女") != -1:\r
+ if "女" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "女"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ if text.find("同声") != -1 or text.find("童") != -1:\r
+ if "童" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "童"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ if text.find("粤") != -1 or text.find("越野") != -1 or text.find("广东话") != -1:\r
+ if "粤" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "粤"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ if text.find("四川") != -1:\r
+ if "四川" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "四川"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ if text.find("个性") != -1:\r
+ if "个性" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "个性"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ if text.find("英语") != -1:\r
+ if "英语" in self.VC_TTS_voice_type:\r
+ self.VC_TTS_voice_key = "英语"\r
+ EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
+ self.AI_TTS_str = "了解,声音已切换"\r
+ return 0\r
+ self.AI_TTS_str = "很抱歉,没找到对应声音"\r
+ return 0\r
+ ret = self._apply_vc_text_intent(desc, cmd, True)\r
+ if ret is not None:\r
+ return ret\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try:\r
+ self.AI_TTS_str = cmd["command"]["fail_prompt"]\r
+ except Exception:\r
+ self.AI_TTS_str = ""\r
+ return -1\r
+\r
+ def check_text(self, text):\r
+ print("check_text:len:{}".format(len(text)))\r
+ self._vc_cmd_diag("check_text_in", text)\r
+ if len(text) > 60:\r
+ print("[VC][cmd] check_text skip len>60")\r
+ return -1\r
+ try:\r
+ if not isinstance(text, str):\r
+ print("[VC][cmd] check_text skip not str type={}".format(type(text).__name__))\r
+ return -1\r
+ ret = self._apply_vc_text_intent(text, None, False)\r
+ if ret is not None:\r
+ print("[VC][cmd] check_text table ret={}".format(ret))\r
+ return ret\r
+ print("[VC][cmd] check_text no_match ret=-1")\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ return -1\r
+\r
+ #获取trace_id\r
+ def get_trace_id(self):\r
+ t = utime.mktime(utime.localtime())\r
+ t = t*1000+urandom.randint(0, 1000)\r
+ md5_data = "{}".format(t)#md5签名\r
+ sign_data = ""\r
+ hash_obj = uhashlib.md5(md5_data)\r
+ res = hash_obj.digest()\r
+\r
+ # 转成十六进制表示\r
+ sign_data = ubinascii.hexlify(res)\r
+ sign_data = sign_data.decode('ascii')\r
+ return sign_data\r
+ \r
+ def get_session_id(self,data = "session_id"):\r
+ t = utime.mktime(utime.localtime())\r
+ t = t*1000+urandom.randint(0, 1000)\r
+ md5_data = "{}{}".format(data,t)#md5签名\r
+ sign_data = ""\r
+ hash_obj = uhashlib.md5(md5_data)\r
+ res = hash_obj.digest()\r
+\r
+ # 转成十六进制表示\r
+ sign_data = ubinascii.hexlify(res)\r
+ sign_data = sign_data.decode('ascii')\r
+ return sign_data\r
+ \r
+ def get_new_message_id(self):\r
+ message_id = self.get_trace_id()+"aaa"+self.get_trace_id()\r
+ self.VC_message_id=message_id[:32]\r
+ return self.VC_message_id \r
+ def get_new_task_id(self):\r
+ task_id = self.get_trace_id()+"aaa"+self.get_trace_id()\r
+ self.VC_task_id=task_id[:32]\r
+ return self.VC_task_id\r
+ #开始播放声音\r
+ def VC_Play_Start(self):\r
+ try:\r
+ if self._ptt_should_skip_remote_playback():\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[VC] VC_Play_Start skip (ptt_interrupt)")\r
+ return\r
+ except Exception:\r
+ pass\r
+ self.tts_in_data_len = 0\r
+ self.tts_out_data_len = 0\r
+ if self.VC_Play_flag == 0:\r
+ try:\r
+ _ptt_fast_open = False\r
+ try:\r
+ if getattr(self, "VC_mode", -1) == 1:\r
+ _ptt_fast_open = True\r
+ except Exception:\r
+ pass\r
+ # 本地提示音/文件播放可能占用同一音频通道,导致 TTS 流播放“写入成功但无声”;\r
+ # 在开启 TTS PCM 前先停掉本地播放,避免资源冲突。\r
+ try:\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ except Exception:\r
+ pass\r
+ # 资源互斥:播放前确保录音上行已停止(无 AEC 场景)\r
+ try:\r
+ if self.record_data_save_flag == 0 and self.VC_record_state_flag != 0:\r
+ self._uplink_request_stop("VC_Play_Start")\r
+ utime.sleep_ms(30)\r
+ except Exception:\r
+ pass\r
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
+ if self.play_pcm == None:\r
+ # 刚停采集时延后打开播音 PCM,且失败时重试,降低「pcm open write only fail」\r
+ last_err = None\r
+ _open_retries = 1 if _ptt_fast_open else 3\r
+ for i in range(_open_retries):\r
+ try:\r
+ if not _ptt_fast_open:\r
+ utime.sleep_ms(80 + i * 60)\r
+ self.play_pcm = audio.Audio.PCM(0, 1, self.VC_play_samplerate, 1, 1, 2)\r
+ ret = self.play_pcm.stream_write_start()\r
+ print("self.record_pcm.stream_write_start:ret:{}".format(ret))\r
+ last_err = None\r
+ break\r
+ except Exception as e:\r
+ last_err = e\r
+ try:\r
+ if self.play_pcm != None:\r
+ self.play_pcm.close()\r
+ except Exception:\r
+ pass\r
+ self.play_pcm = None\r
+ if last_err:\r
+ raise last_err\r
+ elif self.VC_play_TYPE == VC_play_TYPE_MP3:\r
+ pass\r
+ self.VC_Play_flag = 1\r
+ try:\r
+ if not self._ptt_should_skip_remote_playback():\r
+ self._vc_publish_led("playing", "VC_Play_Start/tts")\r
+ except Exception:\r
+ pass\r
+ except Exception as e:\r
+ self.VC_Play_End()\r
+ usys.print_exception(e)\r
+ #暂停/继续声音播放 Pause_flag = 1暂停\r
+ def VC_Play_Pause(self,Pause_flag):\r
+ try:\r
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
+ if self.play_pcm != None: \r
+ self.play_pcm.stream_pause(Pause_flag)\r
+ except Exception as e:\r
+ self.VC_Play_End()\r
+ usys.print_exception(e)\r
+ #停止播放声音\r
+ def VC_Play_End(self):\r
+ self.VC_Play_flag = 0\r
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
+ if self.VC_play_samplerate == 8000:\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ elif self.VC_play_samplerate == 16000:\r
+ if self.play_pcm != None:\r
+ print("Close:play_pcm.close()!!!!!!!!!!!!!!!!!!")\r
+ try:\r
+ self.play_pcm.stream_stop()\r
+ self.play_pcm.close()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.play_pcm=None\r
+ \r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ elif self.VC_play_TYPE == VC_play_TYPE_MP3:\r
+ self.aud.stopPlayStream()\r
+ self.aud.stopAll()\r
+ if self.AI_audio_queue.empty()!=True:\r
+ self.AI_audio_queue.clear()\r
+\r
+ # TTS 播完:按真实 FSM/录音态恢复灯;本地短音不走此路径\r
+ try:\r
+ if getattr(self, "_vc_play_end_suppress_idle_led", 0):\r
+ if self._vc_led_state_debug_enabled():\r
+ print("[LED] VC_Play_End skip led (suppress)")\r
+ elif getattr(self, "VC_record_state_flag", 0) != 0:\r
+ self._vc_publish_led("listening", "VC_Play_End/recording")\r
+ else:\r
+ self._vc_led_sync_from_fsm()\r
+ except Exception:\r
+ pass\r
+ # 关键日志:定位“tts 结束绿灯仍不灭”究竟是 base 态(如 charging/full)还是 playing 未退出\r
+ try:\r
+ if "vc_config" in globals() and vc_config and (\r
+ vc_config.get_bool("LED_DEBUG", False) or vc_config.get_bool("KEY_LED_DEBUG", False) or vc_config.get_bool("HW_STATUS_DEBUG", False)\r
+ ):\r
+ try:\r
+ led = EventMesh.publish("get_led_debug_state")\r
+ except Exception:\r
+ led = None\r
+ print("[VC] VC_Play_End led={}".format(led))\r
+ except Exception:\r
+ pass\r
+ \r
+\r
+ def _1S_timer_cb(self,*argc):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._1S_timer_cb_}) \r
+\r
+ def _fsm_timeout_table_tick(self, now_ms):\r
+ # 兜底统一入口:按“状态机状态 + 超时阈值 + 节流”驱动,避免兜底抢跑/多入口打架。\r
+ # 只在对应 turn/session 状态触发动作。\r
+ try:\r
+ turn = getattr(self.fsm, "turn", VCTurnState.Idle)\r
+ except Exception:\r
+ turn = VCTurnState.Idle\r
+\r
+ # 1) synthesis→tts.started 超时:只在 Thinking 期允许清 pending 并重启会话\r
+ # 根因:服务端未返回 tts.started 时,继续“回到聆听/开麦”会把本轮状态机拉扯得更乱;\r
+ # 这里直接重启会话,让协议重新进入 started→下一轮流程。\r
+ try:\r
+ if turn == VCTurnState.Thinking and self._tts_pending == 1 and self._tts_pending_ms:\r
+ try:\r
+ if self._fb_last_tts_pending_timeout_ms:\r
+ tts_gap_ok = utime.ticks_diff(now_ms, self._fb_last_tts_pending_timeout_ms) > 15000\r
+ else:\r
+ tts_gap_ok = True\r
+ except Exception:\r
+ tts_gap_ok = True\r
+ if utime.ticks_diff(now_ms, self._tts_pending_ms) > 8000 and tts_gap_ok:\r
+ self._fb_last_tts_pending_timeout_ms = now_ms\r
+ print("[VC][tts_pending] timeout, clear pending and restart session")\r
+ self._tts_pending = 0\r
+ self._tts_pending_ms = 0\r
+ if self.VC_mode == 0 and self.Close_flag == 0:\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})\r
+ except Exception:\r
+ pass\r
+\r
+ # 2) thinking 超时:只在「llm.completed 已收到但 tts.started 长时间未到」时才重启会话\r
+ # 注意:不能以 llm.created 的时间戳作为超时基准,否则会在 LLM 正常推理期间误判超时(notts 场景)。\r
+ try:\r
+ if self.VC_mode == 0 and self.Close_flag == 0:\r
+ llm_completed_ms = getattr(self, "_llm_completed_ms", 0)\r
+ # 只有在 llm.completed 之后才允许超时重启\r
+ if turn == VCTurnState.Thinking and llm_completed_ms and self._turn_has_tts_started == 0:\r
+ age_ms = utime.ticks_diff(now_ms, llm_completed_ms)\r
+ restart_threshold_ms = 20000\r
+ # _fb_last_thinking_restart_ms==0 时 ticks_diff(now,0) 非预期(间隔被误判极大),\r
+ # 会在 llm.completed 后首批 tick 就触发 AI_VC_start → notts(无 tts.started);首次仅信任 age 门限\r
+ try:\r
+ if self._fb_last_thinking_restart_ms:\r
+ gap_ok = utime.ticks_diff(now_ms, self._fb_last_thinking_restart_ms) > 15000\r
+ else:\r
+ gap_ok = True\r
+ except Exception:\r
+ gap_ok = True\r
+ if (age_ms > restart_threshold_ms and gap_ok and\r
+ self._tts_pending == 0 and self._need_audio_played_ack == 0 and self.VC_Play_flag == 0):\r
+ self._fb_last_thinking_restart_ms = now_ms\r
+ self._llm_completed_ms = 0\r
+ self._server_vad_silence = 0\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ try:\r
+ print("[VC][thinking_timeout] AI_VC_start age_ms={} thr={}".format(\r
+ age_ms, restart_threshold_ms))\r
+ except Exception:\r
+ pass\r
+ self._vc_led_alert_rb_flash()\r
+ # thinking_timeout 策略:重启会话(而不是抢开麦),避免干扰服务端 LLM/TTS 流程\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})\r
+ except Exception:\r
+ pass\r
+\r
+ def _1S_timer_cb_(self):\r
+ try:\r
+ self._vc_tick_serial_cancel_timeout()\r
+ except Exception:\r
+ pass\r
+\r
+ EventMesh.publish("system_sleep_rest")#防止黑屏\r
+ # --- Level 1:仅长对话(VC_mode==0),无业务下行 idle_sec 后结束会话(需再次按键进入)\r
+ # close_timerout:保活清除策略见 _vc_rx_resets_long_idle_timer(排除文本 ping/pong、heartbeat 等)\r
+ # 与浅休眠/灭屏解耦;单对话不走本计时\r
+ idle_sec = 20\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ idle_sec = int(vc_config.get_int("VC_IDLE_AUTO_CLOSE_SEC", 20))\r
+ except Exception:\r
+ idle_sec = 20\r
+ if idle_sec < 5:\r
+ idle_sec = 5\r
+ if self.VC_mode == 0 and self.VC_satrt_flag == 1 and getattr(self, "AI_client_VC_isconnect", 0) == 1:\r
+ self.close_timerout += 1\r
+ else:\r
+ self.close_timerout = 0\r
+ if self.VC_mode == 0 and idle_sec > 0 and self.close_timerout > idle_sec:\r
+ try:\r
+ print("[VC][idle_L1] 长对话无业务下行超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={} {}".format(\r
+ self.close_timerout, idle_sec, self.VC_mode,\r
+ getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1),\r
+ self._vc_fsm_log_snip()))\r
+ except Exception:\r
+ pass\r
+ if self._vc_long_idle_active_speech_or_later():\r
+ try:\r
+ print("[VC][idle_L1] 处理=websocket_error(对话中无服务端响应)")\r
+ except Exception:\r
+ pass\r
+ print("[VC][long_idle] branch=speech_no_rsp -> websocket_error + ud_error")\r
+ print("接口无响应,超时退出")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"接口超时了。请重试"}) \r
+ self.ud_error(["VC",self.VC_taskid])\r
+ else:\r
+ try:\r
+ print("[VC][idle_L1] 处理=Close(结束对话,需再次按键进入)")\r
+ except Exception:\r
+ pass\r
+ print("[VC][long_idle] branch=idle_timeout -> Close")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ #EventMesh.publish("sleep_BT")#休眠蓝牙\r
+\r
+ # --- Level 2:长对话+单对话,业务 idle 累计 VC_IDLE_DEEP_SLEEP_SEC → 断开(若仍连)+ 深睡事件\r
+ deep_sec = 60\r
+ deep_en = 1\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ deep_sec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))\r
+ deep_en = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))\r
+ except Exception:\r
+ deep_sec, deep_en = 60, 1\r
+ active_deep = bool(deep_en) and deep_sec > 0\r
+ if active_deep and deep_sec < 10:\r
+ deep_sec = 10\r
+ if not active_deep:\r
+ self._idle_deep_sec = 0\r
+ elif not self._vc_is_business_idle():\r
+ self._idle_deep_sec = 0\r
+ try:\r
+ self._vc_idle_deep_armed_suppress = 0\r
+ except Exception:\r
+ pass\r
+ elif getattr(self, "_vc_idle_deep_armed_suppress", 0):\r
+ pass\r
+ else:\r
+ self._idle_deep_sec += 1\r
+ if active_deep and self._idle_deep_sec > deep_sec:\r
+ try:\r
+ print("[VC][idle_L2] 业务idle累计超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={}".format(\r
+ self._idle_deep_sec, deep_sec, self.VC_mode,\r
+ getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1)))\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._vc_idle_deep_armed_suppress = 1\r
+ except Exception:\r
+ pass\r
+ self._idle_deep_sec = 0\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._vc_idle_deep_sleep_impl})\r
+\r
+ # 统一兜底表(按状态机状态触发)\r
+ try:\r
+ self._fsm_timeout_table_tick(utime.ticks_ms())\r
+ except Exception:\r
+ pass\r
+\r
+ # (不做额外兜底)动态切换依赖 FSM 状态正确清理后,由 server.tts.completed→local.play_end 驱动自动开麦\r
+\r
+ # print("get_audio_state !!!!!{} {} {}\r\n".format(self.play_end,EventMesh.publish("get_audio_state"),self.play_flag))\r
+ # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====\r
+ try:\r
+ if getattr(self.fsm, "await_played", False):\r
+ # PTT(VC_mode==1) 也必须走本段:旧逻辑整段包在 VC_mode!=1 内会导致 tts.completed 后仍 await_played,\r
+ # 无人触发 VC_Play_End,绿灯/playing 卡死至按键。\r
+ # 不能只用 stream_is_empty:队列仍可能有待播数据时提前关播会“没声”;in/out 字节在分帧下可能未完全对齐,\r
+ # 在已 tts.completed 且队列已空时,以服务端结束下发 + PCM 空为准。\r
+ is_pcm_empty = (self.play_pcm == None) or (self.play_pcm != None and self.play_pcm.stream_is_empty() == 1)\r
+ try:\r
+ q_empty = self.AI_audio_queue.empty()\r
+ except Exception:\r
+ q_empty = True\r
+ try:\r
+ in_len = getattr(self, "tts_in_data_len", 0)\r
+ out_len = getattr(self, "tts_out_data_len", 0)\r
+ except Exception:\r
+ in_len, out_len = 0, 0\r
+ try:\r
+ tts_done_ms = getattr(self.fsm, "last_tts_completed_ms", 0)\r
+ except Exception:\r
+ tts_done_ms = 0\r
+ drained_strict = (in_len > 0 and out_len >= in_len and q_empty)\r
+ drained_after_server = (bool(tts_done_ms) and q_empty)\r
+ drained = drained_strict or drained_after_server\r
+ # 8s 超时:兜底认为播放结束(避免播放状态卡死)\r
+ too_long = False\r
+ try:\r
+ t0 = getattr(self.fsm, "last_tts_completed_ms", 0)\r
+ if t0:\r
+ too_long = utime.ticks_diff(utime.ticks_ms(), t0) > 8000\r
+ except Exception:\r
+ pass\r
+ if (is_pcm_empty and drained) or too_long:\r
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_End})\r
+ self._fsm_apply_actions(self.fsm.on_local_play_end())\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ except Exception:\r
+ pass\r
+ if self.online_kws == 1:\r
+ if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:#要打开录音上传数据实现kws\r
+ pass\r
+\r
+ # thinking/llm→tts 等兜底已在 _fsm_timeout_table_tick 内统一处理\r
+ # VC WebSocket 文本 ping:间隔由 default_config 的 VC_WS_PING_INTERVAL_SEC 控制(秒),<=0 表示关闭 ping\r
+ try:\r
+ _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 4))\r
+ except Exception:\r
+ _ping_iv = 4\r
+ if _ping_iv > 0:\r
+ self.VC_pingping_timerout += 1\r
+ if self.VC_pingping_timerout >= _ping_iv:\r
+ self.VC_pingping_timerout = 0\r
+ self.AI_client_VC_ping()\r
+ # self.asr_pingping_timerout += 1\r
+ # if self.asr_pingping_timerout>3:\r
+ # self.asr_pingping_timerout=0\r
+ # self.AI_client_asr_ping() \r
+ # 历史遗留:VC_Thinking_timerout 的重启通道会在 llm.created 后抢跑触发 AI_VC_start,\r
+ # 直接打断本轮 LLM→TTS(notts 日志:llm.created 后立刻 AI_VC_start,导致无 tts.started)。\r
+ # 现在统一由 _fsm_timeout_table_tick(Thinking 超时) 负责重启会话,因此禁用该旧通道。\r
+ if self.VC_Thinking_timerout > 0 and self.VC_Thinking_timerout < 100:\r
+ self.VC_Thinking_timerout = 100\r
+ # def AI_kws_start(self): \r
+ # print("AI_kws_start")\r
+ # EventMesh.publish("wakeup_BT")\r
+ # EventMesh.publish("system_sleep_rest")#防止黑屏\r
+ # url="ws://118.145.158.207:22080/wakeup/ws"\r
+ # self.AI_client_kws = uwebsocket.Client.connect(url, debug=True)\r
+ # self.AI_client_VC_recv_id = _thread.start_new_thread(self.AI_client_VC_recv, (self.AI_client_VC,))\r
+ # self.VC_Speech_Start()#开启线程获取录音数据\r
+ #VC开始\r
+ def AI_VC_start(self):\r
+ print("AI_VC_start1")\r
+ \r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ self.VC_satrt_flag = 1\r
+ self.VC_stop_play_audio_flag = 1\r
+ self.VC_satrt_kws_flag = 1\r
+ if self.VC_Thinking_timerout < 100:\r
+ self.VC_Thinking_timerout = 0\r
+ # 不在这里提前切灯态:播放/聆听灯只在“播放/录音真正开始/停止”处更新\r
+ EventMesh.publish("wakeup_BT")\r
+ #self.VC_Play_End()#关闭音频播放\r
+ print("AI_VC_start2")\r
+ #self.aud.aud_tone_play(16,200)#播放按键音\r
+ self.LocalRespondingEnded_timerout = 0\r
+ self.close_timerout = 0\r
+ self._idle_deep_sec = 0\r
+ self._vc_idle_deep_armed_suppress = 0\r
+ \r
+ #\r
+ if self.AI_client_VC_isconnect == 0:#没连接的时候请求开始\r
+ self.websocket_connect_VC()#连接服务器\r
+ \r
+ #utime.sleep_ms(50)\r
+ else:\r
+ print("AI_VC_start2 (already connected) {}".format(self._vc_fsm_log_snip()))\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ if self.AI_client_VC_isconnect == 0:\r
+ #self.Close()\r
+ return\r
+ # 单对话 PTT:会话已 started 时勿重复发 Start(否则第二轮起上行/ASR 易乱)\r
+ _ptt_skip_start = False\r
+ try:\r
+ if self.VC_mode == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
+ _ptt_skip_start = True\r
+ print("[PTT] AI_VC_start skip duplicate Start (session active)")\r
+ except Exception:\r
+ pass\r
+ if not _ptt_skip_start:\r
+ self.AI_client_VC_start_requset()\r
+ print("AI_VC_start3")\r
+ \r
+ if (not _ptt_skip_start) and self.AI_TTS_str == "" and self.AI_new_voice_url == "":\r
+ self._uplink_request_start("AI_VC_start")\r
+ \r
+ \r
+ # pass\r
+ print("AI_VC_start4")\r
+\r
+ #self.VC_Play_Start()\r
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})\r
+ #开始线程\r
+ \r
+ # if self.AI_VC_send_data_queue_thread_id == 0: \r
+ # _thread.stack_size(self.Audio_buf_size+8*1024)\r
+ # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())\r
+ # _thread.stack_size(8*1024)\r
+ # print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))\r
+ \r
+ if self.AI_audio_queue_thread_id ==0:\r
+ _thread.stack_size(24*1024)\r
+ self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())\r
+ _thread.stack_size(8*1024)\r
+ print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))\r
+ else:\r
+ print("AI_VC_start :AI_audio_queue_thread_id != 0 ")\r
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
+ self._1S_timer.start(1000,1,self._1S_timer_cb)\r
+ # self.VC_pingping_timerout = 0\r
+ # self.asr_pingping_timerout = 0\r
+ print("AI_VC_start5")\r
+ #开始会话\r
+ def AI_client_VC_start_requset(self):\r
+ voice="zh_male_wennuanahu_moon_bigtts"\r
+ try:\r
+ voice = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["voice"]\r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ #voice="zh_female_dongmanxiaoyang_moon_bigtts"\r
+ token = EventMesh.publish("get_dev_token")\r
+ if token ==None:\r
+ token = ""\r
+ # bot_option:优先从 default_config(usr 可覆盖)读取 VC_BOT_ID / VC_WORKFLOW_ID,允许空字符串\r
+ _def_bot_id = "7462617881076351002"\r
+ _def_workflow_id = "7462621757327196187"\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ _vc_bot_id = vc_config.get_attr("VC_BOT_ID", _def_bot_id)\r
+ _vc_workflow_id = vc_config.get_attr("VC_WORKFLOW_ID", _def_workflow_id)\r
+ if _vc_bot_id is None:\r
+ _vc_bot_id = ""\r
+ if _vc_workflow_id is None:\r
+ _vc_workflow_id = ""\r
+ else:\r
+ _vc_bot_id, _vc_workflow_id = _def_bot_id, _def_workflow_id\r
+ except Exception:\r
+ _vc_bot_id, _vc_workflow_id = _def_bot_id, _def_workflow_id\r
+ json_data = \\r
+ {\r
+ "header": {\r
+ "event": "Start",\r
+ "trace_id": self.get_trace_id(),\r
+ },\r
+ "payload": {\r
+ "simple_response": True,#不返回llm过程文本\r
+ "device_id": modem.getDevImei(),\r
+ \r
+ "user_agent": "AudioClient/1.0",\r
+ "dialog_attributes": {\r
+ #"provider": "doubao",\r
+ # "provider": "coze",\r
+\r
+ "cfg": {\r
+ "disable_empty_prompt" : True,#拦截空字符请求大模型\r
+ "custom_variables": {\r
+ "name": self.bot_name,\r
+ "devToken":token,\r
+ },\r
+ "greeting":{\r
+ "enabled":False,\r
+ "content":""\r
+ },\r
+ "openai_option":{\r
+ },\r
+\r
+ "bot_option": {\r
+ "use_workflow": True,\r
+ #"input_format": "text",\r
+ "bot_id": _vc_bot_id,\r
+ "workflow_id": _vc_workflow_id,\r
+ \r
+ }\r
+ }\r
+ },\r
+ "outbound_attributes": {\r
+ "provider": "volcengine",\r
+ "cfg": {\r
+ "format": "pcm",\r
+ "sample_rate": self.VC_play_samplerate,\r
+ "bit_rate": 16,\r
+ "channel": 1,\r
+ "volume": self.VC_TTS_volume,\r
+ "emotion": self.emotion,\r
+ #"voice": "7426720361732980745",\r
+ "voice": voice,#音色\r
+ "speech_rate": self.speech_rate,#语速\r
+ "chunk_interval_ms": 10, # 配置每段音频入队间隔时间 100ms\r
+ }\r
+ },\r
+ "inbound_attributes": {\r
+ # "provider": "xun-fei",#\r
+ "provider": "volcengine",\r
+ "disable_vad":True,\r
+ "vad_silence":self.vad_silence,\r
+ "interrupt_options":{\r
+ "concatenate_results":True,\r
+ "thinking_time_only": True,\r
+ #"keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断)\r
+ #"interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表\r
+ },\r
+ "cfg": {\r
+ "format": "pcm",\r
+ "sample_rate": self.VC_record_samplerate,\r
+ "bit_rate": 16,\r
+ "channel": 1,\r
+ "enable_punctuation": True,\r
+ "enable_itn":True,\r
+ "keywords":[]\r
+ }\r
+ },\r
+ "commands":self.commands,\r
+ }\r
+ }\r
+ # "bot_id":"7481164663863459850",#鸭子\r
+ # "workflow_id":"7480485093845991443",\r
+ # "bot_id":"7462617881076351002",#语言助手\r
+ # "workflow_id":"7462621757327196187",\r
+ # "bot_id":"7469964258592931852",#深圳百事通\r
+ # "workflow_id":"7469968083063603241",\r
+ # "bot_id":"7411071327182438412",#柒牌\r
+ # "workflow_id":"7469049680291610636",\r
+ # if self.online_kws == 1:#打开\r
+ # json_data["payload"]["inbound_attributes"]["interrupt_options"]["keyword_interrupt"] = True\r
+ # json_data["payload"]["inbound_attributes"]["interrupt_options"]["interrupt_keywords"] = ["你好,柒先生","你好"]\r
+ #如果\r
+ json_data["payload"]["outbound_attributes"]["provider"]=self.outbound_attributess_provider#TTS音色服务选择,火山\r
+ json_data["payload"]["inbound_attributes"]["provider"]=self.inbound_attributess_provider#ASR服务选择,火山\r
+ #优先选择self.VC_TTS_voice_type[self.VC_TTS_voice_key]里得参数\r
+ # if "inbound_attributess_provider" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
+ # if len(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"])>0:\r
+ # json_data["payload"]["inbound_attributes"]["provider"]=self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"]\r
+ # print("inbound_attributess_provider是VC_TTS_voice_type里的:{}".format(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"]))\r
+ if "outbound_attributess_provider" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
+ if len(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"])>0:\r
+ json_data["payload"]["outbound_attributes"]["provider"]=self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"]\r
+ print("outbound_attributess_provider是VC_TTS_voice_type里的:{}".format(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"]))\r
+\r
+ # 根据配置设置默认 bot/workflow,并允许 VC_TTS_voice_type 覆盖\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ opt = vc_config.get_attr("DIALOG_BOT_OPTION", None)\r
+ if isinstance(opt, dict):\r
+ if "bot_id" in opt:\r
+ json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["bot_id"] = opt.get("bot_id") or ""\r
+ if "workflow_id" in opt:\r
+ json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["workflow_id"] = opt.get("workflow_id") or ""\r
+ if "use_workflow" in opt:\r
+ json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["use_workflow"] = bool(opt.get("use_workflow"))\r
+ except Exception:\r
+ pass\r
+\r
+ try:\r
+ # voice 配置可覆盖 bot_id/workflow_id/openai_option\r
+ cur = self.VC_TTS_voice_type[self.VC_TTS_voice_key]\r
+ if isinstance(cur, dict):\r
+ if cur.get("bot_id"):\r
+ json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["bot_id"] = cur.get("bot_id")\r
+ if "workflow_id" in cur:\r
+ json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["workflow_id"] = cur.get("workflow_id") or ""\r
+ if cur.get("openai_option"):\r
+ json_data["payload"]["dialog_attributes"]["cfg"]["openai_option"] = cur.get("openai_option")\r
+ except Exception:\r
+ pass\r
+\r
+ try:\r
+ _bo = json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]\r
+ if not _bo.get("bot_id", "") or not _bo.get("workflow_id", ""):\r
+ _bo["use_workflow"] = False\r
+ except Exception:\r
+ pass\r
+ try:\r
+ if "vc_config" in globals() and vc_config:\r
+ p = vc_config.get_attr("DIALOG_PROVIDER_DEFAULT", None)\r
+ if p:\r
+ json_data["payload"]["dialog_attributes"]["provider"] = p\r
+ except Exception:\r
+ pass\r
+\r
+ try:\r
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEYWORDS_FROM_VOICE_TYPE", False):\r
+ keywords = []\r
+ for key in self.VC_TTS_voice_type:\r
+ keywords.append(key)\r
+ json_data["payload"]["inbound_attributes"]["cfg"]["keywords"] = keywords\r
+ except Exception:\r
+ pass\r
+ if self.dialog_attributes_provider != None:\r
+ json_data["payload"]["dialog_attributes"]["provider"] = self.dialog_attributes_provider\r
+ if self.commands_mode !=0 :\r
+ json_data["payload"]["commands"] = []\r
+ if self.VC_mode == 0 :#and dev_type != "7PIN_BK_coze":#长对话模式打开vad\r
+ json_data["payload"]["inbound_attributes"]["disable_vad"] = False\r
+ # 新会话开始:在收到服务端 started 前,禁止发送音频帧(避免 session not initialized)\r
+ self._vc_session_started = 0\r
+ try:\r
+ self.fsm.on_session_start_request()\r
+ except Exception:\r
+ pass\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ try:\r
+ _tid = json_data.get("header", {}).get("trace_id", "")\r
+ print("[VC][proto] Start enqueued trace_id={} VC_mode={} disable_vad={} ws_conn={}".format(\r
+ _tid or "-",\r
+ self.VC_mode,\r
+ json_data.get("payload", {}).get("inbound_attributes", {}).get("disable_vad", "?"),\r
+ self.AI_client_VC_isconnect))\r
+ except Exception:\r
+ pass\r
+ \r
+ #开始上传语音\r
+ def AI_client_VC_send_speech(self):\r
+ json_data = \\r
+ {\r
+ "header": {\r
+ "event": "audio.create",\r
+ \r
+ },\r
+ }\r
+ \r
+ if self.VC_mode == 1:#单对话模式\r
+ # 协议门控(可用 VC_PROTO_GUARDS 临时关闭)\r
+ if (not VC_PROTO_GUARDS) or (self._vc_session_started == 1):\r
+ self._ptt_audio_created = 1\r
+ try:\r
+ print("[VC][proto] audio.create send session_started={} proto_guards={}".format(\r
+ self._vc_session_started, VC_PROTO_GUARDS))\r
+ except Exception:\r
+ pass\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ else:\r
+ try:\r
+ print("[VC][proto] audio.create SKIP session_started={} VC_PROTO_GUARDS={} "\r
+ "(等对端 started 后再开麦)".format(self._vc_session_started, VC_PROTO_GUARDS))\r
+ except Exception:\r
+ pass\r
+ \r
+\r
+ \r
+ \r
+ \r
+ \r
+ try:\r
+ self.fsm.on_server_asr_started()\r
+ except Exception:\r
+ pass\r
+ #结束上传语音\r
+ def AI_client_VC_end_speech(self):\r
+ json_data = \\r
+ {\r
+ "header": {\r
+ "event": "audio.completed",\r
+ \r
+ },\r
+ }\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ self._ptt_audio_created = 0\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"audio.completed"})\r
+\r
+ \r
+ #客户端打断对话\r
+ def AI_client_VC_RequestToSpeak(self):\r
+ json_data = \\r
+ {\r
+ "event": "cancel",\r
+ "payload": {}\r
+ }\r
+ print("AI_client_VC_RequestToSpeak!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ # 模式切换等场景 play_tts(..., is_close_VC=1) 会置 Close_flag=1;用户再按 AI 键走 cancel→canceled→\r
+ # uplink_start 时,FSM 与 _uplink_request_start 均会因 Close_flag 拒绝开麦,表现为“切回长对话后按键无上行”。\r
+ try:\r
+ if self.VC_mode == 0:\r
+ self.Close_flag = 0\r
+ self._vc_pending_interrupt_uplink = 1\r
+ self._vc_clear_stale_record_state("RequestToSpeak")\r
+ except Exception:\r
+ pass\r
+ # 只做“打断+清播放队列”,不要在这里关音频队列线程(会导致后续 TTS/提示音不稳定,且触发竞态)\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_init})\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_send,"data":ujson.dumps(json_data)})\r
+ utime.sleep_ms(1)\r
+ try:\r
+ self.fsm.reset_rx_phases()\r
+ except Exception:\r
+ pass\r
+ # #self.Close()\r
+ # self.AI_VC_start() \r
+ #TTS播放\r
+ def AI_client_VC_synthesis(self):\r
+ # 幂等保护:在上一条 synthesis 还未等到 tts.started 前,不重复发(防止多兜底/多入口重复触发)\r
+ try:\r
+ if self._tts_pending == 1:\r
+ return\r
+ except Exception:\r
+ pass\r
+ json_data = \\r
+ {\r
+ "event": "synthesis",\r
+ "payload": {\r
+ "text": self.AI_TTS_str\r
+ }\r
+ }\r
+ # 语义标记只用于“是否自动开麦”的策略。\r
+ # 当前策略:长对话不自动开麦(需按键开始),因此这里不对提示音做自动开麦标记。\r
+ self.AI_TTS_str = ""\r
+ # 标记为“等待 tts.started”,避免在 synthesis→tts.started 窗口误开下一轮录音上行\r
+ self._tts_pending = 1\r
+ try:\r
+ self._tts_pending_ms = utime.ticks_ms()\r
+ except Exception:\r
+ self._tts_pending_ms = 0\r
+ try:\r
+ self.fsm.on_local_synthesis_sent()\r
+ except Exception:\r
+ pass\r
+ print("AI_client_VC_synthesis!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ # if self.VC_state == VC_State_RequestToRespond:\r
+ # print("AI_client_VC_RequestToRespond 重复 tts_str:{}\nasr_str:{}".format(self.AI_TTS_str,self.AI_ASR_str))\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ # 金三 V2.5:AudioPlayed — 端侧 TTS 播报结束后上报,服务端进入下一轮对话(轻度重同步,不断开 WebSocket)\r
+ def AI_client_VC_audio_played(self):\r
+ # V2.5:播完上报;与 VC_PROTO_GUARDS 无关(见文件头说明)\r
+ try:\r
+ if self.AI_client_VC_isconnect != 1 or self.AI_client_VC == None:\r
+ return\r
+ json_data = {"event": "audio.played", "payload": {}}\r
+ print("AI_client_VC_audio_played")\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ def kws_even(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.kws_even_cb}) \r
+ def kws_even_cb(self):\r
+ if EventMesh.publish("get_sos_flag") == 1:\r
+ return\r
+ print("kws_even_cb")\r
+ if self.VC_mode == 1: \r
+ try:\r
+ tts_str = vc_config.get_attr("KWS_PTT_HINT", "请您按住圆形的按钮开始讲话,讲完松开即可 ")\r
+ except Exception:\r
+ tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
+ EventMesh.publish("play_tts",[tts_str,1]) \r
+ return\r
+ if self.VC_mode == 0:\r
+ if EventMesh.publish("get_voice_record_flag") == 1:#停止静默录音\r
+ if EventMesh.publish("get_voice_record_mode") == 2:#语音留言不能被打断\r
+ return\r
+ elif EventMesh.publish("get_voice_record_mode") == 1: #静默录音需要打断\r
+ EventMesh.publish("stop_voice_record")#停止静默录音\r
+ self.aud.aud_tone_play(10,400)\r
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
+ self.Close()\r
+ # if dev_type == "7PIN" :\r
+ # self.aud.aud_tone_play(10,400)#播放按键音\r
+ #self.aud.aud_tone_play(10,200)#播放按键音\r
+ self.AI_TTS_str = ""#"你好,有什么吩咐"\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ self._uplink_request_stop("kws_even_cb")\r
+ self._uplink_request_start("kws_even_cb")\r
+ self.AI_VC_start() \r
+\r
+ def set_vad_silence(self,event = None,msg= None):\r
+ self.vad_silence = msg\r
+ EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence]) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg}) \r
+ #设置tts参数\r
+ def set_wait_first_flag(self,event = None,msg= None):#设置未读语音播报初次记录flag\r
+ self.wait_first_flag = msg\r
+ def set_VC_TTS_voice_type(self,event = None,msg= None):\r
+ self.VC_TTS_voice_type["APP_voice"] = msg\r
+ #self.VC_TTS_voice_type[self.VC_TTS_voice_key]\r
+ self.VC_TTS_voice_key = "APP_voice"\r
+ EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])\r
+ EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])\r
+ print("[VC] TTS_voice key={} map={}".format(self.VC_TTS_voice_key, self.VC_TTS_voice_type))\r
+ #修改音量大小 msg为1则为加大,msg为0则为减小,返回1成功,返回0失败\r
+ def change_volume(self,event =None, msg =None):\r
+ if msg == 0 :\r
+ self.VC_TTS_volume -= 10\r
+ if self.VC_TTS_volume < 0:\r
+ self.VC_TTS_volume = 0\r
+ return 0\r
+ else:\r
+ self.aud.setVolume(self.VC_TTS_volume //10)\r
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
+ EventMesh.publish("post_device_state")\r
+ EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)\r
+ return 1\r
+ elif msg == 1:\r
+ self.VC_TTS_volume += 10\r
+ if self.VC_TTS_volume > 100:\r
+ self.VC_TTS_volume = 100\r
+ return 0\r
+ else:\r
+ self.aud.setVolume(self.VC_TTS_volume //10)\r
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
+ EventMesh.publish("post_device_state")\r
+ EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)\r
+ return 1\r
+ elif msg >= 100:#修改为具体音量\r
+ if msg > 200:\r
+ self.VC_TTS_volume = 100\r
+ return 0\r
+ else:\r
+ self.VC_TTS_volume = msg - 100#还原数据\r
+ self.aud.setVolume(self.VC_TTS_volume //10)\r
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
+ EventMesh.publish("post_device_state")\r
+ EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)\r
+ return 1\r
+ #调节聆听快慢(以秒为单位)\r
+ def NLP_set_vad_silence(self,event,msg):\r
+ if msg == 1:#聆听变快\r
+ self.vad_silence -= 1000\r
+ if self.vad_silence < 1000:\r
+ AI_TTS_str = "已经是最快了" \r
+ self.vad_silence = 1000\r
+ else:\r
+ AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000)\r
+ elif msg == 0:#聆听变慢\r
+ self.vad_silence += 1000\r
+ if self.vad_silence > 4000:\r
+ self.vad_silence = 4000\r
+ #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])\r
+ #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小\r
+ AI_TTS_str = "已经是最慢了"\r
+ else:\r
+ AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000) \r
+ EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])\r
+ return AI_TTS_str\r
+ #查询指令判断\r
+ def find_cmd(self,event ,msg):\r
+ if "电量" in msg:\r
+ battery = EventMesh.publish("get_battery")\r
+ print("电量查询:{}".format(battery))\r
+ tts_play_str = "电量剩余百分之{}".format(battery)\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+ # print("电量!!!!!!!!!!!")\r
+ elif "信号" in msg:\r
+ signal = net.csqQueryPoll()\r
+ tts_play_str = "当前信号{}".format(signal)\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+ # print("信号!!!!!!!!!!!")\r
+ elif "卡号" in msg:\r
+ # print("卡号!!!!!!!!!!!")\r
+ iccid = sim.getIccid()\r
+ iccid = num_str_to_chinese(iccid)#转为中文\r
+ if iccid != -1 and len(iccid) > 5:\r
+ print("iccid{}".format(iccid))\r
+ tts_play_str = "当前卡号为:{}".format(iccid)\r
+ else:\r
+ tts_play_str = "卡号查询失败"\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+ elif "设备号" in msg:\r
+ imei = modem.getDevImei()\r
+ if imei != -1 and len(imei) > 5:\r
+ print("IMEI:{}".format(imei))\r
+ tts_play_str = "当前设备号为:{}".format(imei)\r
+ else:\r
+ tts_play_str = "设备号查询失败"\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+ elif "音量" in msg :\r
+ tts_play_str = "当前音量是{}".format(self.VC_TTS_volume)\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+ elif "语速" in msg :\r
+ tts_play_str = "当前语速是{}".format(EventMesh.publish("find_rate"))\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+ \r
+ #改变音色判断\r
+ def change_voice(self,event ,msg):\r
+ voice = msg\r
+ # if "男声" in msg or "普通话" in msg:\r
+ # self.VC_TTS_voice_key = "男"\r
+ # else:\r
+ # self.VC_TTS_voice_key = voice\r
+ self.outbound_attributess_provider = "xun-fei"\r
+ if "男声" in msg or "普通话" in msg:\r
+ self.VC_TTS_voice_key = "男"\r
+ self.outbound_attributess_provider = "volcengine"\r
+ elif "女声" in msg:\r
+ self.VC_TTS_voice_key = "女"\r
+ self.outbound_attributess_provider = "volcengine"\r
+ elif "童声" in msg:\r
+ self.VC_TTS_voice_key = "童" \r
+ self.outbound_attributess_provider = "volcengine"\r
+ elif "香港粤语" in msg:\r
+ self.VC_TTS_voice_key = "香港粤语"\r
+ elif "四川话" in msg:\r
+ self.VC_TTS_voice_key = "四川" \r
+ elif "上海话" in msg:\r
+ self.VC_TTS_voice_key = "上海" \r
+ elif "安徽合肥话" in msg:\r
+ self.VC_TTS_voice_key = "安徽合肥" \r
+ elif "山东话" in msg:\r
+ self.VC_TTS_voice_key = "山东话" \r
+ elif "陕西话" in msg:\r
+ self.VC_TTS_voice_key = "陕西" \r
+ elif "广东话" in msg:\r
+ self.VC_TTS_voice_key = "广东" \r
+ elif "成都话" in msg:\r
+ self.VC_TTS_voice_key = "成都" \r
+ elif "湖南话" in msg:\r
+ self.VC_TTS_voice_key = "湖南" \r
+ elif "苏州话" in msg:\r
+ self.VC_TTS_voice_key = "苏州" \r
+ elif "东北话" in msg:\r
+ self.VC_TTS_voice_key = "东北" \r
+ elif "河南话" in msg:\r
+ self.VC_TTS_voice_key = "河南" \r
+ elif "湖北话" in msg:\r
+ self.VC_TTS_voice_key = "湖北" \r
+ elif "闽南话" in msg:\r
+ self.VC_TTS_voice_key = "闽南" \r
+ elif "内蒙古话" in msg:\r
+ self.VC_TTS_voice_key = "内蒙古话" \r
+ EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])\r
+ tts_play_str = "声音已改为{}".format(voice)\r
+ EventMesh.publish("play_tts",[tts_play_str,0])\r
+\r
+ #闹钟的url播放接口\r
+ def play_single_voice(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_single_voice_cb,"data":msg}) \r
+ def play_single_voice_cb(self,data):\r
+ try:\r
+ new_voice_url = data[0] #音频地址\r
+ is_close_VC = data[1]\r
+ if new_voice_url != "":\r
+ self.AI_new_voice_url = new_voice_url\r
+ self.alarm_play_flag = 1\r
+ if is_close_VC != None:\r
+ self.Close_flag = is_close_VC\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_single_voice_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ self.play_new_voice_flag = 1\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ pass \r
+ #语音留言\r
+ def play_new_voice(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg}) \r
+ def play_new_voice_cb(self,data):\r
+ try:\r
+ new_voice_url = data[0] #音频地址\r
+ is_close_VC = data[1]\r
+ if new_voice_url != "":\r
+ self.recv_voice = 1#收到留言\r
+ print("已收到最新留言!!!!!")\r
+ # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数\r
+ self.AI_new_voice_url = new_voice_url\r
+ # if self.pwk_cnt_voice == 0 and self.pwk_notice_flag == 0:#并且不在多条留言的状态\r
+ # EventMesh.publish("play_tts",["您有一条语音留言,10秒内按下中间键可以收听",1])\r
+ if self.pwk_notice_flag == 0 and EventMesh.publish("get_app_new_voice_flag"):#不处于播放状态并且是小程序下发的语音而不是请求的\r
+ self.pwk_cnt_voice += 1#加上当前这条\r
+ EventMesh.publish("set_storage_config",["is_noread_voice",1])\r
+ EventMesh.publish("set_app_new_voice_flag",0)\r
+ print("voice_add_to = {}".format(self.pwk_cnt_voice))\r
+ print("self.pwk_notice_flag = {}".format(self.pwk_notice_flag))\r
+ if self.pwk_cnt_voice >1:\r
+ if self.pwk_notice_flag == 0:\r
+ tts_str = "您有" + str(self.pwk_cnt_voice) + "条未读语音留言,10秒内按下中间键可以收听"\r
+ EventMesh.publish("play_tts",[tts_str,1])\r
+ elif self.pwk_cnt_voice == 1:\r
+ if self.pwk_notice_flag == 0:\r
+ tts_str = "您有1条语音留言,10秒内按下中间键可以收听"\r
+ EventMesh.publish("play_tts",[tts_str,1])\r
+ if is_close_VC != None:\r
+ self.Close_flag = is_close_VC\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_new_voice_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ self.play_new_voice_flag = 1\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ pass\r
+ #历史留言查看\r
+ def play_his_voice(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_his_voice_cb,"data":msg}) \r
+ def play_his_voice_cb(self,data):\r
+ try:\r
+ new_voice_url = data[0] #音频地址\r
+ is_close_VC = data[1]\r
+ time = data[2]\r
+ tts_play_str = time + "收到" \r
+ EventMesh.publish("play_tts",[tts_play_str,1])\r
+ if new_voice_url != "":\r
+ self.recv_voice = 1#收到留言\r
+ print("历史留言查看")\r
+ # tts_play_str = "收到" \r
+ # EventMesh.publish("play_tts",[tts_play_str,1])\r
+ # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数\r
+ self.AI_new_voice_url = new_voice_url\r
+ if is_close_VC != None:\r
+ self.Close_flag = is_close_VC\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_his_voice_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ self.play_new_voice_flag = 1\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ pass\r
+ #等待未读留言查看\r
+ def play_wait_voice(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_wait_voice_cb,"data":msg}) \r
+ def play_wait_voice_cb(self,data):\r
+ try:\r
+ new_voice_url = data[0] #音频地址\r
+ is_close_VC = data[1]\r
+ print("是否为第一次:{}".format(self.wait_first_flag))\r
+ if self.wait_first_flag == 0:\r
+ tts_play_str = "您有" + str(len(EventMesh.publish("get_play_new_voice_list"))) + "条未听留言,10秒内按下中间键可以收听"\r
+ EventMesh.publish("play_tts",[tts_play_str,1]) \r
+ # self.wait_first_timer.start(1000,1,self.noread_wait_vocie)\r
+ self.wait_first_flag = 1\r
+ elif self.wait_first_flag == 1:\r
+ self.wait_sort += 1\r
+ self.AI_TTS_str = "第"+str(self.wait_sort)+"条" \r
+ if new_voice_url != "":\r
+ self.recv_voice = 1#收到留言\r
+ print("未读留言查看")\r
+ self.AI_new_voice_url = new_voice_url\r
+ if is_close_VC != None:\r
+ self.Close_flag = is_close_VC\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_wait_voice_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ self.play_new_voice_flag = 1\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ pass\r
+ def noread_wait_vocie(self,*args):#如果一直没读\r
+ self.wait_first_cnt += 1\r
+ print("未读留言等待收听中:{}".format(self.wait_first_cnt))\r
+ if EventMesh.publish("get_recv_voice") == 1:#如果检测到留言播放\r
+ self.wait_first_cnt = 0\r
+ self.wait_first_timer.stop()\r
+ if self.wait_first_cnt == 15:\r
+ EventMesh.publish("set_wait_first_flag",0)\r
+ EventMesh.publish("set_history_voice_flag",0)\r
+ EventMesh.publish("set_wait_voice_flag",0)\r
+ self.wait_first_cnt = 0\r
+ self.wait_first_timer.stop()\r
+ #音乐播放\r
+ def play_music(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_music_cb,"data":msg}) \r
+ def play_music_cb(self,data):\r
+ try:\r
+ music_name = data[0]\r
+ new_voice_url = data[1] #音频地址\r
+ is_close_VC = data[2]\r
+ if new_voice_url != "":\r
+ self.AI_new_voice_url = new_voice_url\r
+ self.music_play_flag = 1#表示音乐播放开启\r
+ EventMesh.publish("play_tts",["下面为您播放歌曲{}".format(music_name),1])\r
+ if is_close_VC != None:\r
+ self.Close_flag = is_close_VC\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_music_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ self.play_new_voice_flag = 1\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+\r
+ #AI翻译接收TTS\r
+ def play_trans_tts(self,event = None,msg= None):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_trans_tts_cb,"data":msg}) \r
+ def play_trans_tts_cb(self,data):\r
+ try:\r
+ new_voice_url = data #音频地址\r
+ if new_voice_url != "":\r
+ self.AI_new_voice_url = new_voice_url\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ if self.AI_ASR_send_data_queue.empty()!=True:\r
+ self.AI_ASR_send_data_queue.clear()\r
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
+ self.AI_ASR_send_buf_len = 0\r
+ self._uplink_request_stop("play_trans_tts_cb")\r
+ self.websocket_deconnect_VC()\r
+ self.AI_VC_start()\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
+ self.play_new_voice_flag = 1\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
+ except Exception as e:\r
+ usys.print_exception(e) \r
+ def AI_client_new_voice(self): #上传未读的语音地址/包括音频地址也可以\r
+ json_data = \\r
+ {\r
+ "event": "audio.convert",\r
+ "payload": {\r
+ "audio_convert_request": {\r
+ "audio_url": self.AI_new_voice_url,\r
+ "bit_depth": 16,\r
+ "channels": 1,\r
+ "sample_rate": 16000,\r
+ "target_format": "pcm"\r
+ }\r
+ }\r
+ }\r
+ self.AI_new_voice_url = ""\r
+ print("AI_client_new_voice!!!!!!!!!!!!!!!!!!!!!!!!!!!!!{}".format(json_data))\r
+ self.AI_client_VC_send(ujson.dumps(json_data))\r
+ \r
+\r
+ def chenggong_tone(self):\r
+ EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
+ # self.aud.aud_tone_play(10,400)\r
+ def send_tone(self):\r
+ EventMesh.publish("audio_montage_play",["send_tone.mp3"])\r
+ def concurrency_limit_exceeded_tone(self):\r
+ self.Close()\r
+ EventMesh.publish("audio_montage_play",["chaobingfatis.mp3"]) #播放超并发提醒\r
+ #给服务器回应ack\r
+\r
+ \r
+ ############################################\r
+ #关闭音频播放线程\r
+ def AI_audio_queue_thread_close(self):\r
+ print("AI_audio_queue_thread_close1!!!!!!!!!!!!!!!!!!!!!!")\r
+ if self.AI_audio_queue_thread_id != 0:\r
+ count =500\r
+ while True:\r
+ count -= 1\r
+ if self.AI_audio_queue_thread_lock == 0 or count <=0:\r
+ break\r
+ else:\r
+ self.AI_audio_queue_thread_lock = 2\r
+ print("AI_audio_queue_thread going:{}...".format(count))\r
+ utime.sleep_ms(1)\r
+ self.AI_audio_queue_thread_lock = 0\r
+ print("AI_audio_queue_thread_close2!!!!!!!!!!!!!!!!!")\r
+ if self.AI_audio_queue_thread_id != 0:\r
+ try:\r
+ _thread.stop_thread(self.AI_audio_queue_thread_id)\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ self.AI_audio_queue_thread_id = 0\r
+ # 音频队列 \r
+ def AI_audio_queue_thread(self):\r
+ print("AI_audio_queue_thread start")\r
+ count = 0\r
+ if self.AI_audio_queue.empty()!=True:\r
+ self.AI_audio_queue.clear()\r
+ print("音频数据还有剩余!!!!!!!!!!!!")\r
+ while True:\r
+ try:\r
+ if self.AI_audio_queue_thread_lock == 2 or self.VC_stop_play_audio_flag == 0:\r
+ print("AI_audio_queue_thread 该退出了")\r
+ break\r
+ #\r
+ count+=1\r
+ if count%500 == 1 and VC_HEARTBEAT_LOG:\r
+ print("AI_audio_queue_thread on")\r
+ utime.sleep_ms(1)\r
+ #print("AI_audio_queue_thread")\r
+ if self.AI_audio_queue.empty() :\r
+ self.AI_audio_queue_thread_lock = 0\r
+ \r
+ \r
+ else:\r
+ self.AI_audio_queue_thread_lock = 1\r
+ data = self.AI_audio_queue.get()\r
+ self.close_timerout = 0\r
+ if self.play_flag == 0:\r
+ self.play_flag =1\r
+ #self.AI_client_tts_data_len=len(data)\r
+ #print("audio_queue:len:{}".format(len(data[0])))\r
+ \r
+ if len(data[0]) > 10:\r
+ try:\r
+ if self._ptt_should_drop_inbound_tts():\r
+ continue\r
+ except Exception:\r
+ pass\r
+ for i in range(5): \r
+ if self.VC_Play_flag == 0:\r
+ self.VC_Play_Start()\r
+ utime.sleep_ms(2)\r
+ else:\r
+ break\r
+ # 打断后 PCM 偶发未打开:加重试,避免「队列有数据、play_pcm=None、无声」\r
+ if self.VC_play_TYPE == VC_play_TYPE_PCM and self.play_pcm is None and self.VC_stop_play_audio_flag == 1:\r
+ for j in range(4):\r
+ if self.VC_Play_flag == 0:\r
+ self.VC_Play_Start()\r
+ utime.sleep_ms(30 + j * 35)\r
+ if self.play_pcm is not None:\r
+ break\r
+ try:\r
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
+ # if self.VC_play_samplerate == 8000:\r
+ # tmp= self.aud.playStream(1,data[0] )\r
+ # else:\r
+ write_buf = bytearray(data[0])\r
+ #print("play_pcm.stream_write1 len:{}".format(len(write_buf)))\r
+ if self.play_pcm != None:\r
+ #print("play_pcm.stream_write !!!")\r
+ ret = self.play_pcm.stream_write(write_buf,len(write_buf))\r
+ #ret= self.aud.playStream(1, data[0])#播放音频流\r
+ if self.tts_out_data_len == 0:\r
+ try:\r
+ sid = getattr(self, "_cur_sid", "")\r
+ did = getattr(self, "_cur_dialog_id", "")\r
+ except Exception:\r
+ sid, did = "", ""\r
+ print("[TTS] first_write ret={} len={} sid={} did={}".format(\r
+ ret, len(write_buf), sid, did\r
+ ))\r
+ if ret <= 0:\r
+ print("[TTS] stream_write ret={} len={} (no audio?)".format(ret, len(write_buf)))\r
+ self.tts_out_data_len += ret\r
+ #print("play_pcm.stream_write2:{}".format(self.tts_out_data_len))\r
+ elif self.VC_play_TYPE == VC_play_TYPE_MP3:\r
+ tmp= self.aud.playStream(3, data[0])#播放音频流\r
+ \r
+ if tmp != 0:\r
+ print("2playStream:error tmp:{} data len:{}!!!!!!!!!!!!".format(tmp,len(data[0])))\r
+ # # #播放失败的时候填充WAV头\r
+ # data_len = len(data[0])\r
+ # data_len_b = data_len.to_bytes(4,'little')#数据长度转16进制数组\r
+ # h_data = [ 0x52 ,0x49 ,0x46 ,0x46 #,0x24 ,0x00 ,0x00 ,0x00 #文件标识、文件大小\r
+ # ,0x57 ,0x41 ,0x56 ,0x45 ,0x66 ,0x6D ,0x74 ,0x20 #文件格式、格式块标识符\r
+ # ,0x10 ,0x00 ,0x00 ,0x00 ,0x01 ,0x00 ,0x01 ,0x00 ,0x80 ,0x3E ,0x00 ,0x00 #格式块大小4、音频格式2、声道数2、采样率4\r
+ # ,0x80 ,0x3E ,0x00 ,0x00 ,0x02 ,0x00 ,0x10 ,0x00 ,0x64 ,0x61 ,0x74 ,0x61]#每秒字节数4、数据块对齐2、位深度2、data4\r
+ # h_data_b = bytes(h_data)\r
+ # file_len = data_len+len(h_data_b)+4+4\r
+ # file_len_b = file_len.to_bytes(4,'little')#文件长度转16进制数组\r
+ # h_data_b = h_data_b[:4]+file_len_b+h_data_b[4:]+data_len_b\r
+ # #self.aud.playStream(2, h_data_b)#播放音频流\r
+ # tmp= self.aud.playStream(2, h_data_b+data[0])#播放音频流\r
+ # print("3playStream:tmp:{} !!!!!!!!!!!!".format(tmp))\r
+ \r
+ \r
+ mem = gc.mem_free()\r
+ #print('1剩余内存RAM:{} KB'.format(mem / 1024))\r
+ except Exception as e:\r
+ print("AI_audio_queue_thread:ERROR:")\r
+ usys.print_exception(e)\r
+ \r
+ # if self.VC_state == VC_State_TTSEnd and self.AI_audio_queue.empty() :#self.AI_audio_queue为空且tts下发结束\r
+ # #通知服务器播放结束\r
+ # self.AI_client_VC_LocalRespondingEnded()\r
+ except Exception as e:\r
+ print("AI_audio_queue_thread:ERROR:")\r
+ usys.print_exception(e)\r
+ self.AI_audio_queue_thread_lock = 0\r
+ self.AI_audio_queue_thread_id = 0\r
+ print("AI_audio_queue_thread:exit")\r
+ def factory_mode(self):\r
+ print("factory_mode")\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.factory_start}) \r
+ def factory_start(self):\r
+ self.factory_timer.start(3000, 1, self.factory_timer_cb)\r
+ def factory_timer_cb(self, *args, **kwargs):\r
+ print("factory_timer_cb play......")\r
+ #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功\r
+ EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])\r
+ def Close_VC(self,event,msg):\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
+ def Close(self):\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] Close() enter VC_mode={} {} VC_satrt_flag={} VC_record_state_flag={}".format(\r
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(),\r
+ getattr(self, "VC_satrt_flag", -1), getattr(self, "VC_record_state_flag", -1)))\r
+ except Exception:\r
+ pass\r
+ self.VC_stop_play_audio_flag = 0\r
+ try:\r
+ self.AI_key_long_press_flag = 0\r
+ except Exception:\r
+ pass\r
+ self.VC_Thinking_timerout = 0\r
+ self.AI_ASR_timer.stop()\r
+ self._1S_timer.stop()\r
+ self.close_timerout = 0\r
+ self._idle_deep_sec = 0\r
+ self.factory_timer.stop()\r
+ self.AI_TTS_str = ""\r
+ self.AI_ASR_str = ""\r
+ self.AI_ASR_str_tmp = ""\r
+ self.AI_ASR_flag = 0\r
+ self.AI_ASR_check_timeout = 0\r
+ self.AI_ASR_error_timeout = 0\r
+ self.LocalRespondingEnded_timerout = 0\r
+ self._need_audio_played_ack = 0\r
+ self._tts_pending = 0\r
+ self._tts_pending_ms = 0\r
+ self._vc_session_started = 0\r
+ self._ptt_audio_created = 0\r
+ self._vc_pending_interrupt_uplink = 0\r
+ try:\r
+ self._skip_started_uplink_once = 0\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._ptt_abort_interrupt_arm()\r
+ except Exception:\r
+ pass\r
+ # Power 短按停止对话后,再次按 AI 需要新建 dialog_id:清理本轮 dialog/session 标识\r
+ try:\r
+ self._cur_dialog_id = ""\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self._cur_sid = ""\r
+ except Exception:\r
+ pass\r
+ self.is_ruppt_tts = 0#打断标志位清零\r
+ print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] step=set_led_state_idle (first)")\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
+ self.tts_in_data_len = 0\r
+ self.tts_out_data_len = 0\r
+ #必须先销毁线程,不然线程会容易报错\r
+ \r
+ try:\r
+ self.AI_audio_queue_thread_close()#销毁音频线程\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ try:\r
+ self.AI_VC_send_data_queue_thread_close()#销毁数据发送线程\r
+ except Exception as e: \r
+ usys.print_exception(e)\r
+ \r
+ # Close 可能被不同线程触发:统一切回主线程执行 stop\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] step=schedule_uplink_stop reason=Close (async publish_AI_btnInfo)")\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"Close"})\r
+ \r
+ print("ai_VC Close1.1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ \r
+ print("ai_VC Close1.2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ \r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] step=websocket_deconnect_VC")\r
+ except Exception:\r
+ pass\r
+ try:\r
+ self.websocket_deconnect_VC()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ \r
+\r
+ \r
+\r
+\r
+ print("ai_VC Close1.3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ \r
+ print("ai_VC Close2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ \r
+ \r
+ \r
+ \r
+ if self.record_data_save_flag == 1:\r
+ if self.record_fd :\r
+ self.record_fd.close()\r
+ self.record_fd = None\r
+ if self.play_data_save_flag == 1:\r
+ if self.play_fd !=None:\r
+ self.play_fd.close()\r
+ self.play_fd = None\r
+ print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")\r
+ \r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+ self.play_new_voice_flag = 0 #语音消息播放状态\r
+ if self.voice_choose_flag == 0: # 如果没有未读语音\r
+ self.AI_new_voice_url = "" # 语音播放地址\r
+ print("关闭并重置语音标志位")\r
+ else:\r
+ print("关闭但是没有重置语音标志位")\r
+ self.AI_ASR_send_buf_len = 0\r
+ #self.AI_WS_connect_Queue.put(self.AI_client_VC)\r
+ try:\r
+ if getattr(self, "VC_mode", 0) != 1:\r
+ print("[VC][long_idle] step=set_led_state_idle (second) -> VC_init VC_satrt_flag=0")\r
+ except Exception:\r
+ pass\r
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
+ self.VC_init()\r
+ self.VC_satrt_flag = 0\r
+ # Level2:会话结束后仍可继续 1s 节拍累计 idle 直至深睡(依赖 VC_IDLE_DEEP_SLEEP_SEC)\r
+ try:\r
+ _dsec = 60\r
+ _den = 1\r
+ if "vc_config" in globals() and vc_config:\r
+ _dsec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))\r
+ _den = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))\r
+ if _den and _dsec > 0:\r
+ self._1S_timer.start(1000, 1, self._1S_timer_cb)\r
+ except Exception:\r
+ pass\r
+ # try:\r
+ # while True:\r
+ # if self.AI_WS_connect_Queue.empty()!=True:\r
+ # AI_WS_connect = self.AI_WS_connect_Queue.get()\r
+ # if AI_WS_connect :\r
+ # try:\r
+ # AI_WS_connect._close()\r
+ # AI_WS_connect = None\r
+ # except Exception as e:\r
+ # usys.print_exception(e)\r
+ # else:\r
+ # print("websocket链接已为空")\r
+ # else:\r
+ # break\r
+ # except Exception as e:\r
+ # usys.print_exception(e)\r
+\r
+ def VC_init2(self):\r
+ self.VC_init()\r
+ \r
+ print("ai_VC VC_init2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
+ try:\r
+ self.websocket_deconnect_VC()\r
+ except Exception as e:\r
+ usys.print_exception(e)\r
+ print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")\r
+ self._1S_timer.stop()\r
+ if self.AI_VC_send_data_queue.empty()!=True:\r
+ self.AI_VC_send_data_queue.clear()\r
+\r
+ self.AI_ASR_send_buf_len = 0\r
+ if self.AI_audio_queue.empty()!=True:\r
+ self.AI_audio_queue.clear()\r
+ def VC_init(self):\r
+ _q0 = self.AI_audio_queue.size()\r
+ self.VC_Play_End()\r
+ if self.AI_audio_queue.empty()!=True:\r
+ self.AI_audio_queue.clear()\r
+ print("[VC] init audio_queue {} -> {}".format(_q0, self.AI_audio_queue.size()))\r
+ # num = EventMesh.publish("get_volum_size")\r
+ # if num == None:\r
+ # num = 8\r
+ # self.aud.setVolume(num)#恢复原始音量\r
+ \r
+def num_str_to_chinese(num_str):\r
+ # 数字与中文映射(含零)\r
+ num_map = {\r
+ '0': '零', '1': '一', '2': '二', '3': '三', '4': '四',\r
+ '5': '五', '6': '六', '7': '七', '8': '八', '9': '九'\r
+ }\r
+ chinese = ''\r
+ # 逐位转换,拼接单位\r
+ for char in num_str:\r
+ if char in num_map:\r
+ digit = num_map[char]\r
+ chinese += digit\r
+ else:\r
+ chinese += char\r
+ \r
+ return chinese\r
+\r