Browse Source

feat(camera): 根据CPU架构自动选择SDK路径并处理可选函数

添加平台检测功能自动选择不同架构的SDK路径
在dahua_sdk.py中实现可选函数绑定机制,缺失函数时降级处理而非崩溃
更新AGENTS.md文档说明SDK路径选择和ARM64兼容性
wenhongquan 4 ngày trước cách đây
mục cha
commit
fe73ebc722
3 tập tin đã thay đổi với 73 bổ sung66 xóa
  1. 10 6
      AGENTS.md
  2. 20 14
      dual_camera_system/config/camera.py
  3. 43 46
      dual_camera_system/dahua_sdk.py

+ 10 - 6
AGENTS.md

@@ -144,12 +144,16 @@ python safety_main.py --panorama-ip 192.168.1.100 --ptz-ip 192.168.1.101
 
 ## 重要注意事项
 
-1. **SDK 路径**: config 中硬编码为 `/home/wen/dsh/dh/Bin`(Linux 路径),macOS 开发需注意。测试设备实际路径为 `/home/admin/dsh/dh/Bin`
-2. **校准间隔**: 实际是 24 小时,不是 README 中的 5 分钟
-3. **模型路径**: 安全检测模型在 `/home/wen/dsh/yolo/yolo11m_safety.pt`
-4. **YOLO11 自动下载**: 首次运行自动下载预训练权重
-5. **OCR 服务**: 需先启动 llama-server(默认 localhost:8111)
-6. **工作模式**: `main.py` 是 OCR 模式,`safety_main.py` 是安全检测模式
+1. **SDK 路径**: config/camera.py 根据 CPU 架构自动选择 SDK 路径:
+   - `aarch64` → `/home/admin/dsh/dh/arm/Bin`(Orange Pi 测试设备)
+   - `x86_64` → `/home/wen/dsh/dh/Bin`(x86 Linux 服务器)
+   - 其他 → 项目相对路径 `../dh/Bin`(开发环境参考)
+2. **ARM64 SDK 兼容性**: ARM64 版 SDK 缺少 `CLIENT_SetVideoProcCallBack` 等函数,`dahua_sdk.py` 已做可选绑定处理,缺失函数运行时降级而非崩溃
+3. **校准间隔**: 实际是 24 小时,不是 README 中的 5 分钟
+4. **模型路径**: 安全检测模型在 `/home/wen/dsh/yolo/yolo11m_safety.pt`
+5. **YOLO11 自动下载**: 首次运行自动下载预训练权重
+6. **OCR 服务**: 需先启动 llama-server(默认 localhost:8111)
+7. **工作模式**: `main.py` 是 OCR 模式,`safety_main.py` 是安全检测模式
 
 ---
 

+ 20 - 14
dual_camera_system/config/camera.py

@@ -1,38 +1,44 @@
 """
 摄像头配置
 """
+import platform
+import os
 
-# 日志配置
 LOG_CONFIG = {
-    'level': 'INFO',           # 日志级别: DEBUG, INFO, WARNING, ERROR
+    'level': 'INFO',
     'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
-    'file': None,              # 日志文件路径,None表示只输出到控制台
-    'max_bytes': 10 * 1024 * 1024,  # 单个日志文件最大大小 (10MB)
-    'backup_count': 5,         # 保留的日志文件数量
+    'file': None,
+    'max_bytes': 10 * 1024 * 1024,
+    'backup_count': 5,
 }
 
-# 全景摄像头配置
 PANORAMA_CAMERA = {
-    'ip': '192.168.20.196',      # 全景摄像头IP
-    'port': 554,                  # RTSP端口
+    'ip': '192.168.20.196',
+    'port': 554,
     'username': 'admin',
     'password': 'Aa1234567',
-    'channel': 1,                # 通道号
+    'channel': 1,
     'rtsp_url': 'rtsp://admin:Aa1234567@192.168.20.196:554/cam/realmonitor?channel=1&subtype=1',
 }
 
-# 球机配置
 PTZ_CAMERA = {
-    'ip': '192.168.20.197',      # 球机IP
-    'port': 554,                 # RTSP端口
+    'ip': '192.168.20.197',
+    'port': 554,
     'username': 'admin',
     'password': 'Aa1234567',
     'channel': 1,
     'rtsp_url': 'rtsp://admin:Aa1234567@192.168.20.197:554/cam/realmonitor?channel=1&subtype=1',
 }
 
-# 大华SDK库路径
+_ARCH = platform.machine()
+if _ARCH == 'aarch64':
+    _SDK_DIR = '/home/admin/dsh/dh/arm/Bin'
+elif _ARCH == 'x86_64':
+    _SDK_DIR = '/home/wen/dsh/dh/Bin'
+else:
+    _SDK_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '..', 'dh', 'Bin')
+
 SDK_PATH = {
-    'lib_path': '/home/admin/dsh/dh/arm/Bin',
+    'lib_path': _SDK_DIR,
     'netsdk': 'libdhnetsdk.so',
 }

+ 43 - 46
dual_camera_system/dahua_sdk.py

@@ -142,7 +142,14 @@ class DahuaSDK:
             raise
     
     def _setup_functions(self):
-        """设置SDK函数签名"""
+        """设置SDK函数签名
+        
+        不同平台/版本的SDK可能缺少部分函数,用可选绑定处理:
+        - 必需函数:缺失则初始化失败
+        - 可选函数:缺失时设为None,运行时检查可用性
+        """
+        
+        # === 必需函数 ===
         
         # CLIENT_Init
         self.sdk.CLIENT_Init.argtypes = [ctypes.c_void_p, c_ulong]
@@ -181,21 +188,32 @@ class DahuaSDK:
         self.sdk.CLIENT_SnapPicture.argtypes = [c_long, POINTER(self.SNAP_PARAMS)]
         self.sdk.CLIENT_SnapPicture.restype = ctypes.c_bool
         
-        # CLIENT_SetSnapRevCallBack
-        self.sdk.CLIENT_SetSnapRevCallBack.argtypes = [ctypes.c_void_p, c_ulong]
-        self.sdk.CLIENT_SetSnapRevCallBack.restype = None
-        
-        # CLIENT_RealPlayEx - 扩展实时预览 (支持回调)
-        self.sdk.CLIENT_RealPlayEx.argtypes = [c_long, c_int, ctypes.c_void_p, c_int]
-        self.sdk.CLIENT_RealPlayEx.restype = c_long
-        
-        # CLIENT_StopRealPlayEx
-        self.sdk.CLIENT_StopRealPlayEx.argtypes = [c_long]
-        self.sdk.CLIENT_StopRealPlayEx.restype = ctypes.c_bool
+        # === 可选函数(某些SDK版本/平台可能缺失)===
+        self._optional_funcs = {}
         
-        # CLIENT_SetVideoProcCallBack - 设置视频回调
-        self.sdk.CLIENT_SetVideoProcCallBack.argtypes = [ctypes.c_void_p, c_ulong]
-        self.sdk.CLIENT_SetVideoProcCallBack.restype = None
+        self._bind_optional('CLIENT_SetSnapRevCallBack', [ctypes.c_void_p, c_ulong], None)
+        self._bind_optional('CLIENT_RealPlayEx', [c_long, c_int, ctypes.c_void_p, c_int], c_long)
+        self._bind_optional('CLIENT_StopRealPlayEx', [c_long], ctypes.c_bool)
+        self._bind_optional('CLIENT_SetVideoProcCallBack', [ctypes.c_void_p, c_ulong], None)
+    
+    def _bind_optional(self, name: str, argtypes: list, restype):
+        """绑定可选SDK函数,缺失时设为None而不报错"""
+        try:
+            func = getattr(self.sdk, name, None)
+            if func is not None:
+                func.argtypes = argtypes
+                func.restype = restype
+                self._optional_funcs[name] = func
+            else:
+                self._optional_funcs[name] = None
+                print(f"SDK可选函数缺失: {name}")
+        except (AttributeError, OSError) as e:
+            self._optional_funcs[name] = None
+            print(f"SDK可选函数不可用: {name} - {e}")
+    
+    def _has_func(self, name: str) -> bool:
+        """检查SDK函数是否可用"""
+        return self._optional_funcs.get(name) is not None
     
     def init(self, disconnect_callback: Callable = None) -> bool:
         """
@@ -280,59 +298,38 @@ class DahuaSDK:
     
     def real_play_ex(self, login_handle: int, channel: int = 0, 
                      use_callback: bool = True) -> Optional[int]:
-        """
-        开始实时预览 (扩展版,支持视频回调)
-        Args:
-            login_handle: 登录句柄
-            channel: 通道号
-            use_callback: 是否使用回调方式获取视频
-        Returns:
-            预览句柄
-        """
         if not self.sdk or login_handle <= 0:
             return None
         
-        if use_callback:
-            # 创建帧缓冲区
+        if not self._has_func('CLIENT_RealPlayEx'):
+            return self.real_play(login_handle, channel)
+        
+        if use_callback and self._has_func('CLIENT_SetVideoProcCallBack'):
             if channel not in self._video_frame_buffers:
                 self._video_frame_buffers[channel] = VideoFrameBuffer()
-            
-            # 设置视频回调
             if self._video_callback is None:
                 self._setup_video_callback()
-            
-            # 使用 RealPlayEx 启动预览 (rType=0 表示标准预览)
             play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0)
         else:
-            play_handle = self.sdk.CLIENT_RealPlay(login_handle, channel, None)
+            play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0) if self._has_func('CLIENT_RealPlayEx') else self.real_play(login_handle, channel)
         
         return play_handle if play_handle > 0 else None
     
     def _setup_video_callback(self):
-        """设置视频数据回调函数"""
-        # 视频回调函数签名:
-        # void CALLBACK RealDataCallBackEx(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, LONG param, LDWORD dwUser)
+        if not self._has_func('CLIENT_SetVideoProcCallBack'):
+            print("SDK不支持CLIENT_SetVideoProcCallBack,视频回调不可用")
+            return
+        
         def video_callback(real_handle: c_long, data_type: c_ulong, 
                           buffer: POINTER(c_ubyte), buf_size: c_ulong,
                           param: c_long, user_data: c_ulong):
-            """视频数据回调 (内部函数)"""
-            # data_type:
-            # 0 = 系统头
-            # 1 = 流数据 (P帧/I帧)
-            # 2 = 音频数据
-            # 3 = 智能信息
-            
-            if data_type == 1 and buf_size > 0:  # 视频流数据
-                # 注意: 这里收到的是编码后的视频流数据,需要解码才能得到图像
-                # 实际解码需要使用 ffmpeg 或 SDK 的解码接口
+            if data_type == 1 and buf_size > 0:
                 pass
         
-        # 创建 ctypes 回调并保存引用
         self._video_callback = CFUNCTYPE(
             None, c_long, c_ulong, POINTER(c_ubyte), c_ulong, c_long, c_ulong
         )(video_callback)
         
-        # 设置 SDK 回调
         self.sdk.CLIENT_SetVideoProcCallBack(self._video_callback, 0)
     
     def get_video_frame_buffer(self, channel: int = 0) -> Optional[VideoFrameBuffer]: