修复20s不待机以及1分钟不休眠问题
authorsnickersun <snickersun@163.com>
Thu, 21 May 2026 10:42:23 +0000 (18:42 +0800)
committersnickersun <snickersun@163.com>
Thu, 21 May 2026 10:42:23 +0000 (18:42 +0800)
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_common.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py

index af3d7c7..ef5443b 100644 (file)
@@ -90,7 +90,7 @@ VC_HEARTBEAT_LOG = 0
 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
+# 长对话 20s 无 TTS idle:仅 tts.* / started / TTS 音频字节重置 close_timerout;asr/vad/llm 不重置。\r
 # 帧级 WebSocket PING/PONG 在 uwebsocket.recv() 内已处理,不会进入 AI_client_VC_recv。\r
 # 金三 V2.5 文档示例:WebSocket 文本帧 "ping" 保活;对端可能回 pong / heartbeat 类事件。\r
 _VC_KEEPALIVE_RX_EVENTS = frozenset(("pong", "ping", "heartbeat", "keepalive"))\r
index 6f640c1..27f4287 100644 (file)
@@ -321,7 +321,9 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             return False
     def _vc_is_business_idle(self):
-        # Level2:LED ai_state 为 idle,且无录音/流播/音乐闹钟等(与 WS ping 无关)
+        # Level2:LED ai_state 为 idle,且无录音/流播/静默录音/音乐闹钟等
+        if self._vc_session_background_active():
+            return False
         try:
             if getattr(self, "music_play_flag", 0) == 1:
                 return False
@@ -365,6 +367,23 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 self.Close()
         except Exception:
             pass
+        try:
+            self._vc_shutdown_idle_services("idle_L2")
+        except Exception:
+            pass
+        try:
+            if not self._vc_is_business_idle():
+                try:
+                    print("[VC][idle_L2] 仍有后台活跃,推迟 SYSTEM_SLEEP")
+                except Exception:
+                    pass
+                try:
+                    self._vc_idle_deep_armed_suppress = 0
+                except Exception:
+                    pass
+                return
+        except Exception:
+            pass
         try:
             utime.sleep_ms(100)
         except Exception:
@@ -380,7 +399,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             pass
     def _vc_rx_resets_long_idle_timer(self, recv_data):
-        """是否应因本帧下行清零 close_timerout(排除保活/心跳,含 V2.5 文本 ping 对端应答)。"""
+        """是否清零 close_timerout:仅 TTS 反馈(tts.* / started / 音频字节),asr/vad/llm 不计。"""
         try:
             if recv_data is None:
                 return False
@@ -397,17 +416,128 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             try:
                 jd = ujson.loads(t)
             except Exception:
-                return True
+                return False
             ev = jd.get("event")
             if ev is None or ev == "":
                 ev = jd.get("type")
             if ev is None:
+                return False
+            evl = str(ev).strip().lower()
+            if evl in _VC_KEEPALIVE_RX_EVENTS:
+                return False
+            if evl == "started" or evl.startswith("tts."):
                 return True
-            if str(ev).strip().lower() in _VC_KEEPALIVE_RX_EVENTS:
+            return False
+        except Exception:
+            return False
+    def _vc_long_idle_timer_armed(self):
+        """长对话 L1:会话已 started 且已连接时才开始计 20s 无 TTS。"""
+        try:
+            if getattr(self, "VC_mode", -1) != 0:
+                return False
+            if getattr(self, "VC_satrt_flag", 0) != 1:
+                return False
+            if getattr(self, "AI_client_VC_isconnect", 0) != 1:
+                return False
+            if getattr(self, "_vc_session_started", 0) != 1:
                 return False
             return True
         except Exception:
-            return True
+            return False
+    def _vc_session_background_active(self):
+        """是否仍有 AI 会话/采播/静默录音等需保持活跃的后台。"""
+        try:
+            if getattr(self, "VC_satrt_flag", 0) == 1:
+                return True
+            if getattr(self, "AI_client_VC_isconnect", 0) == 1:
+                return True
+            if getattr(self, "VC_record_state_flag", 0) != 0:
+                return True
+            if getattr(self, "VC_Play_flag", 0) != 0:
+                return True
+            if getattr(self, "music_play_flag", 0) == 1:
+                return True
+            if getattr(self, "alarm_play_flag", 0) == 1:
+                return True
+            if EventMesh.publish("get_voice_record_flag") == 1:
+                return True
+        except Exception:
+            pass
+        return False
+    def _vc_shutdown_idle_services(self, reason="idle"):
+        """业务 idle/深睡前关停后台服务(静默录音、TTS 播、VC 链路等)。"""
+        try:
+            print("[VC][idle_shutdown] reason={} VC_satrt={} ws={} rec={} play={} voice_rec={}".format(
+                reason,
+                getattr(self, "VC_satrt_flag", -1),
+                getattr(self, "AI_client_VC_isconnect", -1),
+                getattr(self, "VC_record_state_flag", -1),
+                getattr(self, "VC_Play_flag", -1),
+                EventMesh.publish("get_voice_record_flag"),
+            ))
+        except Exception:
+            pass
+        try:
+            EventMesh.publish("stop_voice_record")
+        except Exception:
+            pass
+        try:
+            self.VC_stop_play_audio_flag = 0
+        except Exception:
+            pass
+        try:
+            self.VC_Play_End()
+        except Exception:
+            pass
+        try:
+            if getattr(self, "aud", None) is not None:
+                self.aud.stopPlayStream()
+                self.aud.stopAll()
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_record_state_flag", 0) != 0:
+                self._uplink_request_stop(reason)
+        except Exception:
+            pass
+        try:
+            if getattr(self, "AI_client_VC_isconnect", 0) == 1:
+                self.websocket_deconnect_VC()
+        except Exception:
+            pass
+        try:
+            EventMesh.publish("sleep_BT")
+        except Exception:
+            pass
+        try:
+            self.VC_pingping_timerout = 0
+            self.VC_satrt_kws_flag = 0
+        except Exception:
+            pass
+    def _vc_prepare_for_idle_close(self):
+        """结束对话进 idle:先停麦/清 FSM 聆听锁,避免 Close 后仍录音或 listen_hold 高电流。"""
+        try:
+            self.Close_flag = 1
+        except Exception:
+            pass
+        try:
+            self._vc_pending_interrupt_uplink = 0
+            self.is_ruppt_tts = 0
+            self._drop_tts_audio = 0
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_record_state_flag", 0) != 0:
+                self._uplink_request_stop("idle_close_prepare")
+        except Exception:
+            pass
+        try:
+            if hasattr(self, "fsm") and self.fsm is not None:
+                self.fsm.speech_uplink_open = 0
+                self.fsm.turn = VCTurnState.Idle
+                self.fsm.await_played = False
+        except Exception:
+            pass
     def _handle_pwk_short_release(self, event=None, msg=None, merged_ai_tail=False):
         """电源键短按:读留言标志、退会议/翻译;否则 Close,或单键模式下走 AI_key_release。"""
         print("pwk_key_release")
@@ -481,6 +611,11 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         return False
 
     def get_vc_hold_listening_led(self, event=None, msg=None):
+        try:
+            if getattr(self, "_vc_idle_close_in_progress", 0) == 1:
+                return 0
+        except Exception:
+            pass
         try:
             return 1 if self._vc_should_hold_listening_led() else 0
         except Exception:
@@ -595,7 +730,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         if self._ai_key_press_long_dialogue_session():
             return True
         if self._ai_key_press_long_dialogue_wake():
-            EventMesh.publish(constants.EVENT.SYSTEM_UP)
+            EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
             return True
         print("[KEY] AI_key_press fallback -> AI_VC_start ws={} started={}".format(
             getattr(self, "AI_client_VC_isconnect", -1),
@@ -1104,7 +1239,10 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         except Exception:
             pass
         try:
-            if self._vc_should_hold_listening_led():
+            if (
+                getattr(self, "_vc_idle_close_in_progress", 0) == 0
+                and self._vc_should_hold_listening_led()
+            ):
                 if ai_state != "listening":
                     if self._vc_led_state_debug_enabled():
                         print("[LED] publish coerce ai={} -> listening reason={} (listen_hold)".format(
@@ -1609,6 +1747,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         self.close_timerout = 0
         self._idle_deep_sec = 0
         self._vc_idle_deep_armed_suppress = 0
+        self._vc_idle_close_in_progress = 0
+        self._vc_idle_l1_close = 0
         self.LocalRespondingEnded_timerout = 0
 
         self.VC_Play_flag = 0
@@ -2267,10 +2407,15 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         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 等)
-        # 与浅休眠/灭屏解耦;单对话不走本计时
+        _bg_active = False
+        try:
+            _bg_active = self._vc_session_background_active()
+        except Exception:
+            pass
+        if _bg_active:
+            EventMesh.publish("system_sleep_rest")
+        # --- Level 1:仅长对话,idle_sec 内无 TTS 下行 → Close 进 idle(需再按 AI 进入)
+        # close_timerout:仅 tts.* / started / TTS 音频重置;见 _vc_rx_resets_long_idle_timer
         idle_sec = 20
         try:
             if "vc_config" in globals() and vc_config:
@@ -2279,35 +2424,28 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             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:
+        if self._vc_long_idle_timer_armed():
             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(
+                print("[VC][idle_L1] 长对话无TTS反馈超时: 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")#休眠蓝牙
+            try:
+                print("[VC][idle_L1] 处理=Close(无TTS反馈进idle,需再次按键进入)")
+            except Exception:
+                pass
+            print("[VC][long_idle] branch=no_tts_timeout -> Close")
+            try:
+                self._vc_idle_l1_close = 1
+            except Exception:
+                pass
+            EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
 
         # --- Level 2:长对话+单对话,业务 idle 累计 VC_IDLE_DEEP_SLEEP_SEC → 断开(若仍连)+ 深睡事件
         deep_sec = 60
@@ -2347,35 +2485,32 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             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 驱动自动开麦
+        if _bg_active:
+            # 统一兜底表(按状态机状态触发)
+            try:
+                self._fsm_timeout_table_tick(utime.ticks_ms())
+            except Exception:
+                pass
 
-        # print("get_audio_state !!!!!{} {} {}\r\n".format(self.play_end,EventMesh.publish("get_audio_state"),self.play_flag))
-        # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====
-        try:
-            self._vc_try_play_end_tick()
-        except Exception:
-            pass
-        if self.online_kws == 1:
-            if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:#要打开录音上传数据实现kws
+            # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====
+            try:
+                self._vc_try_play_end_tick()
+            except Exception:
                 pass
+            if self.online_kws == 1:
+                if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:
+                    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()
+            # VC WebSocket 文本 ping
+            try:
+                _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 4))
+            except Exception:
+                _ping_iv = 4
+            if _ping_iv > 0 and getattr(self, "AI_client_VC_isconnect", 0) == 1:
+                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
@@ -2548,6 +2683,15 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
     def Close_VC(self,event,msg):
         EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) 
     def Close(self):
+        try:
+            self._vc_idle_close_in_progress = 1
+        except Exception:
+            pass
+        try:
+            if getattr(self, "VC_mode", 0) == 0:
+                self._vc_prepare_for_idle_close()
+        except Exception:
+            pass
         try:
             if getattr(self, "VC_mode", 0) != 1:
                 print("[VC][long_idle] Close() enter VC_mode={} {} VC_satrt_flag={} VC_record_state_flag={}".format(
@@ -2628,7 +2772,10 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 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"})
+        try:
+            self._uplink_request_stop("Close")
+        except Exception as e:
+            usys.print_exception(e)
         
         print("ai_VC Close1.1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
         
@@ -2683,6 +2830,10 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
         EventMesh.publish("set_led_state", {"ai_state": "idle"})
         self.VC_init()
         self.VC_satrt_flag = 0
+        try:
+            self._vc_shutdown_idle_services("Close")
+        except Exception:
+            pass
         # Level2:会话结束后仍可继续 1s 节拍累计 idle 直至深睡(依赖 VC_IDLE_DEEP_SLEEP_SEC)
         try:
             _dsec = 60
@@ -2694,6 +2845,11 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
                 self._1S_timer.start(1000, 1, self._1S_timer_cb)
         except Exception:
             pass
+        try:
+            self._vc_idle_close_in_progress = 0
+            self._vc_idle_l1_close = 0
+        except Exception:
+            pass
     def VC_init2(self):
         self.VC_init()
         
index 3e89c97..47cc7fc 100644 (file)
@@ -1172,8 +1172,7 @@ class VCSessionIoMixin:
                     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
+                        # recv 超时:无下行,勿清零(否则 20s 无 TTS 无法进 idle)
                         pass
                     else:
                         usys.print_exception(e)