فهرست منبع

feat(hookvision): 添加了新的RFID、Modbus和RTSP播放器功能。

lsfoo 4 ماه پیش
والد
کامیت
735015ca41
3فایلهای تغییر یافته به همراه330 افزوده شده و 0 حذف شده
  1. 45 0
      hookvision/modbus_client.py
  2. 163 0
      hookvision/rfid_client.py
  3. 122 0
      hookvision/rtsp_player.py

+ 45 - 0
hookvision/modbus_client.py

@@ -0,0 +1,45 @@
+import logging
+from pymodbus.client import ModbusTcpClient
+import threading
+import time
+from typing import Callable, List
+
+logger = logging.getLogger(__name__)
+
+class ModbusClient:
+    def __init__(self, config, on_data_callback: Callable[[List[bool]], None]):
+        self.config = config
+        self.on_data_callback = on_data_callback
+        self.client: ModbusTcpClient = None
+        self.running = False
+        self.thread = None
+
+    def connect(self) -> bool:
+        try:
+            self.client = ModbusTcpClient(self.config.ip, port=self.config.port)
+            self.client.connect()
+            self.running = True
+            self.thread = threading.Thread(target=self._poll_loop, daemon=True)
+            self.thread.start()
+            logger.info(f"Modbus连接成功 {self.config.ip}:{self.config.port}")
+            return True
+        except Exception as e:
+            logger.error(f"Modbus连接失败: {e}")
+            return False
+
+    def disconnect(self):
+        self.running = False
+        if self.client:
+            self.client.close()
+            logger.info("Modbus已断开")
+
+    def _poll_loop(self):
+        while self.running:
+            try:
+                result = self.client.read_coils(0, 100)
+                if result and not result.isError():
+                    bools = [result.bits[i] for i in range(len(result.bits))]
+                    self.on_data_callback(bools)
+            except Exception as e:
+                logger.error(f"Modbus读取错误: {e}")
+            time.sleep(self.config.frequency / 1000.0)

+ 163 - 0
hookvision/rfid_client.py

@@ -0,0 +1,163 @@
+import socket
+import threading
+import time
+import logging
+from typing import Callable, Optional
+
+logger = logging.getLogger(__name__)
+
+class RFIDClient:
+    def __init__(self, config, on_tag_callback: Callable[[Optional[str]], None],
+                 on_state_changed=None, tag_offset: int = -6, tag_length: int = 6, hex_output: bool = True):
+        self.config = config
+        self.on_tag_callback = on_tag_callback
+        self.tag_offset = tag_offset
+        self.tag_length = tag_length
+        self.hex_output = hex_output
+        self.sock: Optional[socket.socket] = None
+        self.running = False
+        self._last_tag: Optional[str] = None
+        self._stable_count = 0
+        self._last_time = time.time()
+        self._last_receive_time = time.time()  # 最后一次收到数据的时间
+        self._reconnect_interval = 5           # 重连尝试间隔(秒)
+        self._last_connect_attempt = 0
+        self.thread = None
+        self.buffer = b""
+        self.on_state_changed = on_state_changed
+
+
+    def connect(self) -> bool:
+        """尝试建立连接,若已连接则直接返回 True,否则尝试重连(有间隔限制)"""
+        if self.sock:
+            return True
+        # 避免频繁重连
+        if time.time() - self._last_connect_attempt < self._reconnect_interval:
+            return False
+        self._last_connect_attempt = time.time()
+        try:
+            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            self.sock.settimeout(3)  # 设置接收超时,避免阻塞
+            self.sock.connect((self.config.ip, self.config.port))
+            self.running = True
+            self.thread = threading.Thread(target=self._read_loop, daemon=True)
+            self.thread.start()
+            self._last_receive_time = time.time()
+            logger.info(f"RFID连接成功 {self.config.ip}:{self.config.port}")
+            if self.on_state_changed:
+                self.on_state_changed(True)
+            return True
+        except Exception as e:
+            logger.error(f"RFID连接失败: {e}")
+            self.sock = None
+            if self.on_state_changed:
+               self.on_state_changed(False)
+            return False
+
+
+    def disconnect(self):
+        """主动断开连接"""
+        self.running = False
+        if self.sock:
+            self.sock.close()
+            self.sock = None
+        logger.info("RFID已断开")
+        if self.on_state_changed:
+            self.on_state_changed(False)
+
+    def _read_loop(self):
+        while self.running and self.sock:
+            try:
+                data = self.sock.recv(1024)
+                if not data:
+                    break
+                self._last_receive_time = time.time()  # 收到任何数据都更新时间
+                self.buffer += data
+                while True:
+                    start = self.buffer.find(b"CT")
+                    if start == -1:
+                        break
+                    next_start = self.buffer.find(b"CT", start + 2)
+                    if next_start == -1:
+                        break
+                    packet = self.buffer[start:next_start]
+                    self.buffer = self.buffer[next_start:]
+                    self._process_packet(packet)
+            except socket.timeout:
+                continue  # 超时不影响循环
+            except Exception as e:
+                logger.error(f"RFID读取错误: {e}")
+                break
+        # 退出循环(连接断开)
+        self.sock = None
+        logger.info("RFID读取循环结束,将触发重连")
+        if self.on_state_changed:
+            self.on_state_changed(False)
+        self.on_tag_callback(None)  # 通知上层
+
+    def _process_packet(self, packet: bytes):
+        hex_str = packet.hex()
+        logger.debug(f"RFID包 长度 {len(packet)} 十六进制: {hex_str}")
+
+        offset = self.tag_offset
+        length = self.tag_length
+        if offset < 0:
+            offset = len(packet) + offset
+        if offset < 0 or offset + length > len(packet):
+            logger.debug("提取范围越界,跳过")
+            return
+
+        tag_bytes = packet[offset:offset + length]
+        if self.hex_output:
+            tag = tag_bytes.hex().upper()
+        else:
+            try:
+                tag = tag_bytes.decode('ascii')
+            except UnicodeDecodeError:
+                tag = tag_bytes.hex().upper()
+
+        if len(tag) < 4 or tag.strip('0') == '':
+            logger.debug("无效卡号(空或全0),忽略")
+            return
+
+        logger.debug(f"提取卡号: {tag}")
+        self._process_tag(tag)
+
+    def _process_tag(self, tag: str):
+        now = time.time()
+        if tag == self._last_tag:
+            self._stable_count += 1
+        else:
+            self._last_tag = tag
+            self._stable_count = 1
+        self._last_time = now
+        logger.debug(f"稳定: 标签 {tag}, 计数 {self._stable_count}/{self.config.stability_count}")
+        if self._stable_count >= self.config.stability_count:
+            logger.info(f"触发稳定标签: {tag}")
+            self.on_tag_callback(tag)
+            self._stable_count = 0
+
+    def check_idle(self):
+        """
+        外部定时(每秒)调用的维护方法:
+        1. 检查空闲超时(卡离开)
+        2. 检查连接状态,若断开或长时间无数据则自动重连
+        """
+        # 空闲超时
+        if self._last_tag and (time.time() - self._last_time) > self.config.stability_time:
+            logger.info("空闲超时,清空状态")
+            self._last_tag = None
+            self._stable_count = 0
+            self.on_tag_callback(None)
+
+        # 连接状态维护
+        if self.sock is None:
+            # 如果 socket 已断开,尝试重连
+            if time.time() - self._last_connect_attempt > self._reconnect_interval:
+                self.connect()
+        else:
+            # 如果 socket 存在但长时间未收到任何数据(10秒),认为连接僵死,主动断开并重连
+            if (time.time() - self._last_receive_time) > 10:
+                logger.warning("RFID长时间未收到数据,主动重连")
+                self.disconnect()
+                # 下次 check_idle 会触发重连

+ 122 - 0
hookvision/rtsp_player.py

@@ -0,0 +1,122 @@
+import av
+import numpy as np
+import time
+import cv2
+import logging
+from PySide6.QtCore import QThread, Signal, QMutex
+from PySide6.QtGui import QImage, QPixmap
+from typing import List, Dict, Any
+
+logger = logging.getLogger(__name__)
+
+class RTSPPlayer(QThread):
+    frame_ready = Signal(QPixmap)
+    first_frame_received = Signal()
+    timeout = Signal()
+
+    def __init__(self, rtsp_url: str, target_width: int = 640, target_height: int = 360, timeout_sec: float = 5.0):
+        super().__init__()
+        self.rtsp_url = rtsp_url
+        self.target_width = target_width
+        self.target_height = target_height
+        self.timeout_sec = timeout_sec
+        self.running = False
+        self.ai_objects: List[Dict[str, Any]] = []
+        self.target_fps = 25
+        self.mutex = QMutex()
+
+    def set_target_size(self, width: int, height: int):
+        self.mutex.lock()
+        self.target_width = width
+        self.target_height = height
+        self.mutex.unlock()
+
+    def set_ai_objects(self, objects):
+        self.ai_objects = objects
+
+    def run(self):
+        self.running = True
+        options = {
+            'rtsp_transport': 'tcp',
+            'buffer_size': '1024',
+            'max_delay': '50000',
+            'flags': 'low_delay',
+            'fflags': 'nobuffer+discardcorrupt',
+            'analyzeduration': '100000',
+            'probesize': '100000',
+            'timeout': str(int(self.timeout_sec * 1000000)),
+            'stimeout': str(int(self.timeout_sec * 1000000)),
+        }
+        start_time = time.time()
+        first_packet = True
+        container = None
+
+        try:
+            container = av.open(self.rtsp_url, options=options)
+            video_stream = container.streams.video[0]
+            video_stream.thread_type = 'AUTO'
+            video_stream.thread_count = 1
+
+            last_emit = 0
+            frame_interval = 1.0 / self.target_fps
+
+            for packet in container.demux(video_stream):
+                if not self.running:
+                    break
+                if first_packet:
+                    first_packet = False
+                    self.first_frame_received.emit()
+
+                for frame in packet.decode():
+                    if not self.running:
+                        break
+                    img = frame.to_rgb().to_ndarray()
+                    now = time.time()
+                    if now - last_emit >= frame_interval:
+                        # 先缩放到目标尺寸
+                        self.mutex.lock()
+                        tw, th = self.target_width, self.target_height
+                        self.mutex.unlock()
+                        if tw > 0 and th > 0 and (img.shape[1] != tw or img.shape[0] != th):
+                            img = cv2.resize(img, (tw, th), interpolation=cv2.INTER_LINEAR)
+
+                        # 再在缩放后的图像上绘制 AI 标注
+                        for obj in self.ai_objects:
+                            x = obj.get('x', 0)
+                            y = obj.get('y', 0)
+                            w = obj.get('w', 0)
+                            h = obj.get('h', 0)
+                            label = obj.get('label', '')
+                            color = obj.get('color', (0, 255, 0))
+                            cv2.rectangle(img, (x, y), (x+w, y+h), color, 2)
+                            cv2.putText(img, label, (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
+
+                        # 转换为 QPixmap
+                        h, w, ch = img.shape
+                        bytes_per_line = ch * w
+                        qt_img = QImage(img.data, w, h, bytes_per_line, QImage.Format_RGB888)
+                        pixmap = QPixmap.fromImage(qt_img)
+                        self.frame_ready.emit(pixmap)
+                        last_emit = now
+
+                if first_packet and (time.time() - start_time) > self.timeout_sec:
+                    logger.warning("RTSP连接超时(未收到任何包)")
+                    self.timeout.emit()
+                    self.running = False
+                    break
+
+                if not self.running:
+                    break
+
+        except Exception as e:
+            logger.error(f"RTSP错误: {e}")
+            if first_packet:
+                self.timeout.emit()
+        finally:
+            if container:
+                container.close()
+            self.running = False
+
+    def stop(self):
+        self.running = False
+        self.wait()