Browse Source

修复6个稳定性问题

1. 语音队列 maxsize=32,满时静默丢弃(防 OOM)
2. 摄像头 ping 线程加 is_alive 保护,防止线程堆积
3. RFID 缓冲区上限 65536,防恶意数据撑爆内存
4. lambda 默认参数捕获正确 player 引用,避免旧对象回调
5. _save_config 使用存储的 _config_path,消除路径推导不一致
6. MQTT 消息处理 30ms 限速,防止队列积压
lsfoo 3 tháng trước cách đây
mục cha
commit
5e8bc83299
3 tập tin đã thay đổi với 28 bổ sung và 16 xóa
  1. 25 15
      hookvision/main_window.py
  2. 2 0
      hookvision/rfid_client.py
  3. 1 1
      main.py

+ 25 - 15
hookvision/main_window.py

@@ -36,9 +36,10 @@ class MainWindow(QMainWindow):
     voice_signal = Signal(str)
     camera_signal_signal = Signal(dict)
 
-    def __init__(self, config: Config):
+    def __init__(self, config: Config, config_path: str = ""):
         super().__init__()
         self.config = config
+        self._config_path = config_path
         self.buckets = config.buckets
         self.active_bucket: BucketModel = None
         self.switch_mode = SwitchMode.AUTO
@@ -79,7 +80,7 @@ class MainWindow(QMainWindow):
         self.ai_enabled = True
         self._last_left_draw: list = []
         self._last_right_draw: list = []
-        self._voice_queue = queue.Queue()
+        self._voice_queue = queue.Queue(maxsize=32)
         self._voice_thread = threading.Thread(target=self._voice_worker, daemon=True)
         self._voice_thread.start()
         self._voices_dir = self._get_voices_dir()
@@ -91,11 +92,13 @@ class MainWindow(QMainWindow):
         self._rtsp_retry_delay_left: int = 1
         self._rtsp_retry_delay_right: int = 1
         self._last_mqtt_connect_attempt: float = 0.0
+        self._last_mqtt_process: float = 0.0
 
         # 摄像头信号监控
         self._camera_signal: dict = {}  # ip → 延迟(ms), -1=不可达
         self._bucket_signal_labels: dict = {}  # bucket.id → QLabel
         self._last_signal_check: float = 0.0
+        self._signal_check_thread: threading.Thread = None
 
         # 连接跨线程信号到主线程槽
         self.rfid_tag_signal.connect(self._on_rfid_tag_impl)
@@ -454,7 +457,10 @@ class MainWindow(QMainWindow):
         self.voice_signal.emit(msg)
 
     def _speak_impl(self, msg: str):
-        self._voice_queue.put(msg)
+        try:
+            self._voice_queue.put_nowait(msg)
+        except queue.Full:
+            pass
 
     @staticmethod
     def _get_voices_dir() -> str:
@@ -694,8 +700,8 @@ class MainWindow(QMainWindow):
         timeout = self.config.video.rtsp_timeout
         player = RTSPPlayer(cam.rtsp_url, w, h, timeout_sec=timeout)
         player.frame_ready.connect(label.setPixmap)
-        player.first_frame_received.connect(lambda: self._on_rtsp_retry_success(side))
-        player.timeout.connect(lambda: self.on_player_timeout(label, player, side))
+        player.first_frame_received.connect(lambda s=side: self._on_rtsp_retry_success(s))
+        player.timeout.connect(lambda l=label, p=player, s=side: self.on_player_timeout(l, p, s))
         old = self.left_player if side == "left" else self.right_player
         if old:
             try:
@@ -866,8 +872,8 @@ class MainWindow(QMainWindow):
                 timeout = self.config.video.rtsp_timeout
                 player = RTSPPlayer(left_cam.rtsp_url, w, h, timeout_sec=timeout)
                 player.frame_ready.connect(self.left_label.setPixmap)
-                player.first_frame_received.connect(lambda: self.hide_loading_on_label(self.left_label))
-                player.timeout.connect(lambda: self.on_player_timeout(self.left_label, player, "left"))
+                player.first_frame_received.connect(lambda p=player: self.hide_loading_on_label(self.left_label))
+                player.timeout.connect(lambda p=player: self.on_player_timeout(self.left_label, p, "left"))
                 self.left_player = player
                 player.start()
 
@@ -877,8 +883,8 @@ class MainWindow(QMainWindow):
                 timeout = self.config.video.rtsp_timeout
                 player = RTSPPlayer(right_cam.rtsp_url, w, h, timeout_sec=timeout)
                 player.frame_ready.connect(self.right_label.setPixmap)
-                player.first_frame_received.connect(lambda: self.hide_loading_on_label(self.right_label))
-                player.timeout.connect(lambda: self.on_player_timeout(self.right_label, player, "right"))
+                player.first_frame_received.connect(lambda p=player: self.hide_loading_on_label(self.right_label))
+                player.timeout.connect(lambda p=player: self.on_player_timeout(self.right_label, p, "right"))
                 self.right_player = player
                 player.start()
         else:
@@ -1000,6 +1006,10 @@ class MainWindow(QMainWindow):
             return
         if not self.ai_enabled:
             return
+        now = time.time()
+        if now - self._last_mqtt_process < 0.03:
+            return
+        self._last_mqtt_process = now
         for cam in self.active_bucket.cameras:
             if cam.topic == topic:
                 self.update_video_size()
@@ -1199,7 +1209,9 @@ class MainWindow(QMainWindow):
         # 每 30 秒检测一次摄像头信号
         if now - self._last_signal_check > 30:
             self._last_signal_check = now
-            threading.Thread(target=self._check_all_cameras, daemon=True).start()
+            if not getattr(self, '_signal_check_thread', None) or not self._signal_check_thread.is_alive():
+                self._signal_check_thread = threading.Thread(target=self._check_all_cameras, daemon=True)
+                self._signal_check_thread.start()
 
     def _collect_camera_ips(self) -> list:
         ips = set()
@@ -1276,16 +1288,14 @@ class MainWindow(QMainWindow):
     def _save_config(self):
         import configparser
         cfg = configparser.ConfigParser()
-        config_name = "config.dev.ini" if "--prod" not in sys.argv else "config.prod.ini"
-        config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), config_name)
-        cfg.read(config_path, encoding="utf-8")
+        cfg.read(self._config_path, encoding="utf-8")
         if not cfg.has_section("AI"):
             cfg.add_section("AI")
         val = "true" if self.config.ai.tts_enabled else "false"
         cfg.set("AI", "tts_enabled", val)
-        with open(config_path, "w", encoding="utf-8") as f:
+        with open(self._config_path, "w", encoding="utf-8") as f:
             cfg.write(f)
-        logger.info(f"TTS 设置已保存: {config_path} → tts_enabled={val}")
+        logger.info(f"TTS 设置已保存: {self._config_path} → tts_enabled={val}")
 
     @staticmethod
     def _autostart_config_dir():

+ 2 - 0
hookvision/rfid_client.py

@@ -84,6 +84,8 @@ class RFIDClient:
                     break
                 self._last_receive_time = time.time()   # 收到任何数据都更新时间
                 self.buffer += data
+                if len(self.buffer) > 65536:
+                    self.buffer = self.buffer[-32768:]
                 while True:
                     start = self.buffer.find(b"CT")
                     if start == -1:

+ 1 - 1
main.py

@@ -64,7 +64,7 @@ def main():
     else:
         config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), config_name)
     config = load_config(config_path)
-    window = MainWindow(config)
+    window = MainWindow(config, config_path)
     window.show()
     sys.exit(app.exec())