Quellcode durchsuchen

fix(camera): 修正摄像头端口配置并更新文档

分离SDK登录端口和RTSP流端口,分别使用37777和554
更新AGENTS.md文档说明端口配置和SDK类型映射
修正dahua_sdk.py中的类型定义以匹配Linux平台
wenhongquan vor 4 Tagen
Ursprung
Commit
caad81c540

+ 8 - 5
AGENTS.md

@@ -149,11 +149,14 @@ python safety_main.py --panorama-ip 192.168.1.100 --ptz-ip 192.168.1.101
    - `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` 是安全检测模式
+3. **SDK 类型映射**: 大华 SDK 在 Linux 上 `DWORD=unsigned int(4B)`, `LONG=int(4B)`, `LLONG=long(8B)`,ctypes 绑定必须严格匹配,否则结构体对齐错误导致登录失败
+4. **初始化顺序**: `main.py` 中先加载 YOLO/PyTorch,再初始化大华 SDK。大华 SDK 的 `CLIENT_Init` 会修改进程内存映射,如果先于 PyTorch 加载会导致 segfault
+5. **校准间隔**: 实际是 24 小时,不是 README 中的 5 分钟
+6. **模型路径**: 安全检测模型在 `/home/wen/dsh/yolo/yolo11m_safety.pt`
+7. **YOLO11 自动下载**: 首次运行自动下载预训练权重
+8. **OCR 服务**: 需先启动 llama-server(默认 localhost:8111)
+9. **工作模式**: `main.py` 是 OCR 模式,`safety_main.py` 是安全检测模式
+10. **摄像头端口**: SDK 登录用 37777,RTSP 流用 554,config 中 `port=37777`, `rtsp_port=554`
 
 ---
 

+ 4 - 2
dual_camera_system/config/camera.py

@@ -14,7 +14,8 @@ LOG_CONFIG = {
 
 PANORAMA_CAMERA = {
     'ip': '192.168.20.196',
-    'port': 554,
+    'port': 37777,
+    'rtsp_port': 554,
     'username': 'admin',
     'password': 'Aa1234567',
     'channel': 1,
@@ -23,7 +24,8 @@ PANORAMA_CAMERA = {
 
 PTZ_CAMERA = {
     'ip': '192.168.20.197',
-    'port': 554,
+    'port': 37777,
+    'rtsp_port': 554,
     'username': 'admin',
     'password': 'Aa1234567',
     'channel': 1,

+ 60 - 30
dual_camera_system/dahua_sdk.py

@@ -7,7 +7,13 @@ import ctypes
 import os
 import threading
 import queue
-from ctypes import c_int, c_long, c_ulong, c_char_p, c_ubyte, POINTER, Structure, byref, CFUNCTYPE
+from ctypes import c_int, c_long, c_uint32, c_char_p, c_ubyte, POINTER, Structure, byref, CFUNCTYPE
+
+# 大华SDK Linux类型映射 (dhnetsdk.h非Windows定义):
+# DWORD = unsigned int (4 bytes) → c_uint32
+# LONG  = int (4 bytes) → c_int
+# LLONG = long (8 bytes on LP64) → c_long
+# LDWORD = long (8 bytes on LP64) → c_long
 from typing import Optional, Callable, Tuple, Dict, Any
 
 
@@ -73,24 +79,48 @@ class DahuaSDK:
     def _setup_structures(self):
         """设置SDK所需的C结构体"""
         
-        # 登录输入参数
+        # 登录输入参数 (必须与SDK头文件 NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY 严格对齐)
         class NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY(Structure):
             _fields_ = [
-                ('dwSize', c_ulong),
-                ('szIP', ctypes.c_char * 64),
-                ('nPort', c_int),
-                ('szUserName', ctypes.c_char * 64),
-                ('szPassword', ctypes.c_char * 64),
-                ('emSpecCap', c_int),
+                ('dwSize', c_uint32),                    # DWORD = uint32
+                ('szIP', ctypes.c_char * 64),            # IP
+                ('nPort', c_int),                        # 端口
+                ('szUserName', ctypes.c_char * 64),      # 用户名
+                ('szPassword', ctypes.c_char * 64),      # 密码
+                ('emSpecCap', c_int),                    # 登录模式
+                ('byReserved', ctypes.c_ubyte * 4),      # 对齐保留
+                ('pCapParam', ctypes.c_void_p),          # 扩展参数
+                ('emTLSCap', c_int),                     # TLS模式
+                ('szLocalIP', ctypes.c_char * 64),       # 本地IP
+            ]
+        
+        # 设备信息 (NET_DEVICEINFO_Ex)
+        class NET_DEVICEINFO_Ex(Structure):
+            _fields_ = [
+                ('sSerialNumber', ctypes.c_ubyte * 48),  # 序列号
+                ('nAlarmInPortNum', c_int),               # 报警输入数
+                ('nAlarmOutPortNum', c_int),              # 报警输出数
+                ('nDiskNum', c_int),                       # 硬盘数
+                ('nDVRType', c_int),                      # DVR类型
+                ('nChanNum', c_int),                       # 通道数
+                ('byLimitLoginTime', ctypes.c_ubyte),      # 限制登录时间
+                ('byLeftLogTimes', ctypes.c_ubyte),        # 剩余登录次数
+                ('bReserved', ctypes.c_ubyte * 2),        # 保留
+                ('nLockLeftTime', c_int),                  # 锁定剩余时间
+                ('Reserved', ctypes.c_char * 4),           # 保留
+                ('nNTlsPort', c_int),                      # TLS端口
+                ('nKeyFrameEncrypt', c_int),               # 关键帧加密
+                ('emAlgorithm', c_int),                    # 加密算法
+                ('Reserved2', ctypes.c_char * 8),          # 保留
             ]
         
-        # 登录输出参数
+        # 登录输出参数 (必须与SDK头文件严格对齐)
         class NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY(Structure):
             _fields_ = [
-                ('dwSize', c_ulong),
-                ('pstuDeviceInfo', ctypes.c_void_p),
-                ('nError', c_int),
-                ('byRes', ctypes.c_char * 128),
+                ('dwSize', c_uint32),                                    # DWORD = uint32
+                ('stuDeviceInfo', NET_DEVICEINFO_Ex),                  # 设备信息
+                ('nError', c_int),                                      # 错误码
+                ('byReserved', ctypes.c_char * 132),                    # 保留
             ]
         
         # 抓拍参数
@@ -151,50 +181,50 @@ class DahuaSDK:
         
         # === 必需函数 ===
         
-        # CLIENT_Init
-        self.sdk.CLIENT_Init.argtypes = [ctypes.c_void_p, c_ulong]
+        # CLIENT_Init(BOOL (CALLBACK *cbDisConnect), LDWORD)
+        self.sdk.CLIENT_Init.argtypes = [ctypes.c_void_p, c_long]
         self.sdk.CLIENT_Init.restype = ctypes.c_bool
         
-        # CLIENT_Cleanup
+        # CLIENT_Cleanup()
         self.sdk.CLIENT_Cleanup.argtypes = []
         self.sdk.CLIENT_Cleanup.restype = None
         
-        # CLIENT_LoginWithHighLevelSecurity
+        # CLIENT_LoginWithHighLevelSecurity -> LLONG
         self.sdk.CLIENT_LoginWithHighLevelSecurity.argtypes = [
             POINTER(self.NET_IN_LOGIN),
             POINTER(self.NET_OUT_LOGIN)
         ]
         self.sdk.CLIENT_LoginWithHighLevelSecurity.restype = c_long
         
-        # CLIENT_Logout
+        # CLIENT_Logout(LLONG)
         self.sdk.CLIENT_Logout.argtypes = [c_long]
         self.sdk.CLIENT_Logout.restype = ctypes.c_bool
         
-        # CLIENT_RealPlay
+        # CLIENT_RealPlay(LLONG, int, void*) -> LLONG
         self.sdk.CLIENT_RealPlay.argtypes = [c_long, c_int, ctypes.c_void_p]
         self.sdk.CLIENT_RealPlay.restype = c_long
         
-        # CLIENT_StopRealPlay
+        # CLIENT_StopRealPlay(LLONG) -> BOOL
         self.sdk.CLIENT_StopRealPlay.argtypes = [c_long]
         self.sdk.CLIENT_StopRealPlay.restype = ctypes.c_bool
         
-        # CLIENT_DHPTZControlEx
+        # CLIENT_DHPTZControlEx(LLONG, int, DWORD, LONG, LONG, LONG, BOOL)
         self.sdk.CLIENT_DHPTZControlEx.argtypes = [
-            c_long, c_int, c_ulong, c_long, c_long, c_long, ctypes.c_bool
+            c_long, c_int, c_uint32, c_int, c_int, c_int, ctypes.c_bool
         ]
         self.sdk.CLIENT_DHPTZControlEx.restype = ctypes.c_bool
         
-        # CLIENT_SnapPicture
+        # CLIENT_SnapPicture(LLONG, SNAP_PARAMS*) -> BOOL
         self.sdk.CLIENT_SnapPicture.argtypes = [c_long, POINTER(self.SNAP_PARAMS)]
         self.sdk.CLIENT_SnapPicture.restype = ctypes.c_bool
         
         # === 可选函数(某些SDK版本/平台可能缺失)===
         self._optional_funcs = {}
         
-        self._bind_optional('CLIENT_SetSnapRevCallBack', [ctypes.c_void_p, c_ulong], None)
+        self._bind_optional('CLIENT_SetSnapRevCallBack', [ctypes.c_void_p, c_long], 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)
+        self._bind_optional('CLIENT_SetVideoProcCallBack', [ctypes.c_void_p, c_long], None)
     
     def _bind_optional(self, name: str, argtypes: list, restype):
         """绑定可选SDK函数,缺失时设为None而不报错"""
@@ -229,7 +259,7 @@ class DahuaSDK:
         if disconnect_callback:
             # 保存回调引用,防止被垃圾回收
             self._disconnect_callback = ctypes.CFUNCTYPE(
-                None, c_long, c_char_p, c_int, c_ulong
+                ctypes.c_bool, c_long, c_char_p, c_int, c_long
             )(disconnect_callback)
         
         result = self.sdk.CLIENT_Init(self._disconnect_callback, 0)
@@ -320,14 +350,14 @@ class DahuaSDK:
             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):
+        def video_callback(real_handle: c_long, data_type: c_uint32, 
+                          buffer: POINTER(c_ubyte), buf_size: c_uint32,
+                          param: c_long, user_data: c_long):
             if data_type == 1 and buf_size > 0:
                 pass
         
         self._video_callback = CFUNCTYPE(
-            None, c_long, c_ulong, POINTER(c_ubyte), c_ulong, c_long, c_ulong
+            None, c_long, c_uint32, POINTER(c_ubyte), c_uint32, c_long, c_long
         )(video_callback)
         
         self.sdk.CLIENT_SetVideoProcCallBack(self._video_callback, 0)

+ 2 - 2
dual_camera_system/panorama_camera.py

@@ -126,7 +126,7 @@ class PanoramaCamera:
         """
         if rtsp_url is None:
             # 构建RTSP地址
-            rtsp_url = f"rtsp://{self.config['username']}:{self.config['password']}@{self.config['ip']}:554/h264/ch{self.config['channel']}/main/av_stream"
+            rtsp_url = f"rtsp://{self.config['username']}:{self.config['password']}@{self.config['ip']}:{self.config.get('rtsp_port', 554)}/h264/ch{self.config['channel']}/main/av_stream"
         
         try:
             self.rtsp_cap = cv2.VideoCapture(rtsp_url)
@@ -213,7 +213,7 @@ class PanoramaCamera:
     
     def _build_rtsp_url(self) -> str:
         """构建 RTSP URL"""
-        return f"rtsp://{self.config['username']}:{self.config['password']}@{self.config['ip']}:554/h264/ch{self.config['channel']}/main/av_stream"
+        return f"rtsp://{self.config['username']}:{self.config['password']}@{self.config['ip']}:{self.config.get('rtsp_port', 554)}/h264/ch{self.config['channel']}/main/av_stream"
     
     def _rtsp_stream_worker(self):
         """RTSP视频流工作线程"""