尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
海康工业三相机联动串口触发系统:从 0 到 1 的踩坑笔记
作者小小张日期2025-07-24 这篇博文不是“Hello World”而是一篇“Hello 三相机 串口 回调 外部触发”的笔记。 如果你也正好被“海康工业相机 Python 多相机同步 串口发字符”的组合折磨过希望下面的 3000 字能帮你少掉几根头发。## 1. 需求背景一句话讲清楚 **“电脑连 3 台海康相机USB/GiGE 都行只要相机被外部信号触发一次就立刻通过串口往外发一个字符3 台相机互不干扰。”**听起来简单过程却踩了 5 个大坑1. 海康官方 Python Demo 只有单相机。2. Windows 下 Python 多相机回调容易“串台”。3. 官方 SDK 的“外部触发”必须**先关闭取流**才能配置否则报错。4. USB 相机与 GiGE 相机 API 混用时枚举顺序每次启动都可能变。5. 串口句柄必须**在回调线程里重新判空**否则偶发 OSError: exception: access violation。---## 2. 最终效果运行脚本后控制台出现串口就绪!!找到:3个相机usb相机:0 相机名:正相机 相机序列号: 18137521usb相机:1 相机名:反相机 相机序列号: 18137635usb相机:2 相机名:带相机 相机序列号: 18138184默认0/1/2代表 正/反/三 [3个相机] 如正确 输入一个数字[按回车]:按回车后3 台相机同时开始取流并进入回调模式。此时用 PLC / 按钮 / 信号发生器给相机触发一次终端立刻出现1 M1 m1 O同时串口 COM3 会依次吐出字符 M、m、O对应 0/1/2 号相机。按任意键退出相机会优雅地关闭。---## 3. 关键实现拆解### 3.1 三相机枚举 序列号绑定海康的 Python SDK 枚举结果顺序**不固定**用序列号硬绑定最稳pythoncam1_nConnectionNum next(i for i in range(deviceList.nDeviceNum)if 18137521 in str(deviceList.pDeviceInfo[i]))cam2_nConnectionNum ...### 3.2 注册回调的正确姿势官方 Demo 里 RegisterImageCallBackEx 只能注册一次多相机必须**每实例注册一次**pythonCALL_BACK_0 FrameInfoCallBack(image_callback_0)CALL_BACK_1 FrameInfoCallBack(image_callback_1)CALL_BACK_2 FrameInfoCallBack(image_callback_2)cam.MV_CC_RegisterImageCallBackEx(CALL_BACK_0, None)cam2.MV_CC_RegisterImageCallBackEx(CALL_BACK_1, None)cam3.MV_CC_RegisterImageCallBackEx(CALL_BACK_2, None)### 3.3 触发模式配置顺序必须先 MV_CC_SetEnumValue(TriggerMode, MV_TRIGGER_MODE_OFF) 把触发关掉再设置 TriggerSource, TriggerActivation 等参数最后 MV_CC_SetEnumValue(TriggerMode, MV_TRIGGER_MODE_ON)。否则设置会失败返回 0x80000010参数无效。### 3.4 串口线程安全回调函数里直接 ser.write(...) 偶尔会崩加锁 判空pythondef Ser_send(send_data):if ser and ser.is_open:try:ser.write(send_data.encode())except Exception as e:print(串口写入失败, e)---## 4. 完整代码可直接运行# -- coding: utf-8 -- r 实现功能: 打开3个海康工业相机 回调模式 支持 网口gige USB 进入回调后就串口输出字符 支持外部触发 备注:3个相机和1个串口同时打开才能正常运行 名称:小小张 python3.8 thonny import random import os,sys,time,msvcrt from ctypes import * #主要靠的ctypes 导C语言的包 sys.path.append(os.getenv(MVCAM_COMMON_RUNENV) /Samples/Python/MvImport) #运行环境依赖海康的python包 mvs4.5.1安装后 可独立运行 from MvCameraControl_class import * R_list[M,N,m,n,O,S] # 打开 COM3波特率 1152008 数据位无校验1 停止位超时 0.5 秒 import serial serserial.Serial(COM3,115200,bytesizeserial.EIGHTBITS,parityserial.PARITY_NONE,stopbitsserial.STOPBITS_ONE,timeout0.5) #winsows系统使用com连接串行口 if (ser.isOpen()):print(串口就绪!!) def port_close(): ser.close() if (ser.isOpen()): print(关闭失败) def Ser_send(send_data): if (ser.isOpen()): ser.write(send_data.encode(utf-8)) #utf-8 编码发送 #ser.write(binascii.a2b_hex(send_data)) #Hex发送 return send_data r flag_a False flag_b False flag_c False # 函数 A每次调用将 flag_a 取反 #【新增三相机回调函数定义】 winfun_ctype WINFUNCTYPE stFrameInfo POINTER(MV_FRAME_OUT_INFO_EX) pData POINTER(c_ubyte) FrameInfoCallBack winfun_ctype(None, pData, stFrameInfo, c_void_p) #C指针转成 py对 # 0 号相机的回调 def image_callback_0(pData, pFrameInfo, pUser): global flag_a flag_a not flag_a stFrameInfo cast(pFrameInfo, POINTER(MV_FRAME_OUT_INFO_EX)).contents if stFrameInfo: print(f {stFrameInfo.nFrameNum1} {Ser_send(M)} ) # 1 号相机的回调 def image_callback_1(pData, pFrameInfo, pUser): global flag_b flag_b not flag_b stFrameInfo cast(pFrameInfo, POINTER(MV_FRAME_OUT_INFO_EX)).contents if stFrameInfo: print(f {stFrameInfo.nFrameNum1} {Ser_send(m)} ) #random.choice([O, S]) # 2 号相机的回调 def image_callback_2(pData, pFrameInfo, pUser): global flag_c flag_c not flag_c stFrameInfo cast(pFrameInfo, POINTER(MV_FRAME_OUT_INFO_EX)).contents if stFrameInfo: print(f {stFrameInfo.nFrameNum1} {Ser_send(O)} ) CALL_BACK_0 FrameInfoCallBack(image_callback_0) CALL_BACK_1 FrameInfoCallBack(image_callback_1) CALL_BACK_2 FrameInfoCallBack(image_callback_2) # if __name__ __main__: # ch:初始化SDK | en: initialize SDK MvCamera.MV_CC_Initialize() deviceList MV_CC_DEVICE_INFO_LIST() tlayerType (MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE) # ch:枚举设备 | en:Enum device ret MvCamera.MV_CC_EnumDevices(tlayerType, deviceList) if ret ! 0: print(枚举设备0个) if deviceList.nDeviceNum 0: input(枚举设备0个:) print (f找到:{deviceList.nDeviceNum}个相机) for i in range(0, deviceList.nDeviceNum): mvcc_dev_info cast(deviceList.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents if mvcc_dev_info.nTLayerType MV_GIGE_DEVICE or mvcc_dev_info.nTLayerType MV_GENTL_GIGE_DEVICE: print (\ngige device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) nip1 ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0xff000000) 24) nip2 ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0x00ff0000) 16) nip3 ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0x0000ff00) 8) nip4 (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0x000000ff) print (current ip: %d.%d.%d.%d\n % (nip1, nip2, nip3, nip4)) elif mvcc_dev_info.nTLayerType MV_USB_DEVICE:# 这里usb 开始 print (fusb相机:{i}) strModeName for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (f相机名:{strModeName}) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (f相机序列号: {strSerialNumber}) pass# usb 在这 elif mvcc_dev_info.nTLayerType MV_GENTL_CAMERALINK_DEVICE: print (\nCML device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (user serial number: %s % strSerialNumber) elif mvcc_dev_info.nTLayerType MV_GENTL_CXP_DEVICE: print (\nCXP device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stCXPInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stCXPInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (user serial number: %s % strSerialNumber) elif mvcc_dev_info.nTLayerType MV_GENTL_XOF_DEVICE: print (\nXoF device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stXoFInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stXoFInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (user serial number: %s % strSerialNumber) print (f默认0/1/2代表 正/反/三 [3个相机] 如正确 输入一个数字[按回车]:) print (f不正确 请修改顺序) nConnectionNum input(默认0/1/2代表 正/反/三 [3个相机] 如果正确 输入0[按回车]:) if int(nConnectionNum) deviceList.nDeviceNum: print (intput error!) # ch:创建相机实例 | en:Creat Camera Object cam MvCamera();cam2 MvCamera();cam3 MvCamera() r 串口就绪!! 找到:3个相机 usb相机:0 相机名:正相机 相机序列号: 18137521 usb相机:1 相机名:反相机 相机序列号: 18137635 usb相机:2 相机名:带相机 相机序列号: 18138184 相机名:正相机0 相机名:反相机1 相机名:带相机2 根据情况修改下面3个参数 从而实现相机对应 cam1_nConnectionNum0 cam2_nConnectionNum1 cam3_nConnectionNum2 # ch:选择设备并创建句柄 | en:Select device and create handle stDeviceList cast(deviceList.pDeviceInfo[int(cam1_nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents ret cam.MV_CC_CreateHandle(stDeviceList) stDeviceList2 cast(deviceList.pDeviceInfo[int(cam2_nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents ret cam2.MV_CC_CreateHandle(stDeviceList2) stDeviceList3 cast(deviceList.pDeviceInfo[int(cam3_nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents ret cam3.MV_CC_CreateHandle(stDeviceList3) #【新增打开三相机】 ret cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0) if ret ! 0: print (f1相机已经打开) ret cam2.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0) if ret ! 0: print (f2相机已经打开) ret cam3.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0) if ret ! 0: print (f3相机已经打开) # # ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera) if stDeviceList.nTLayerType MV_GIGE_DEVICE or stDeviceList.nTLayerType MV_GENTL_GIGE_DEVICE: nPacketSize cam.MV_CC_GetOptimalPacketSize() if int(nPacketSize) 0: ret cam.MV_CC_SetIntValue(GevSCPSPacketSize,nPacketSize) if ret ! 0: print (Warning: Set Packet Size fail! ret[0x%x] % ret) else: print (Warning: Get Packet Size fail! ret[0x%x] % nPacketSize) r input(任意键继续) # ch:设置触发模式为off | en:Set trigger mode as off #设置触发模式 #设置 触发极性 #设置 触发 消抖时间 #设置 相机的相关参数 # 设置曝光# 设置增益 exposureTime130;gain11; fValue 30; for h in [cam, cam2, cam3]: h.MV_CC_SetEnumValue(TriggerMode, MV_TRIGGER_MODE_OFF)#3相机触发模式 h.MV_CC_SetBoolValue(TriggerCacheEnable, True);# 凭感觉 打开这个 可以缓存一个触发信号 h.MV_CC_SetFloatValue(ExposureTime, float(exposureTime)) h.MV_CC_SetFloatValue(Gain, float(gain)) h.MV_CC_SetFloatValue(AcquisitionFrameRate, float(fValue));#设置相机30帧 #h.MV_CC_SetEnumValue(TriggerMode, 1)#触发模式打开 #h.MV_CC_SetEnumValue(TriggerSource, 7)#触发线路Line0 #h.MV_CC_SetEnumValue(TriggerActivation, 0)#触发极性 上升沿 #参数 采集控制 r 开硬件触发 虚拟相机肯定不进回调 #【新增为三相机分别注册回调】 cam.MV_CC_RegisterImageCallBackEx(CALL_BACK_0, None) cam2.MV_CC_RegisterImageCallBackEx(CALL_BACK_1, None) cam3.MV_CC_RegisterImageCallBackEx(CALL_BACK_2, None) # #【新增三相机同时开始取流】 cam.MV_CC_StartGrabbing();cam2.MV_CC_StartGrabbing();cam3.MV_CC_StartGrabbing() # # 只要不开始取流 mvs可以设置的参数都可以设 # 打开相机后停止取流状态可以mvs可以设置的参数都可以设 # 有些参数 开始取流 或取流过程中也可以设置 # 读取参数 只要相机打开就能读. print (按任意键 停止取流!) msvcrt.getch()# # 阻塞 #【新增三相机同时停止取流】 cam.MV_CC_StopGrabbing();cam2.MV_CC_StopGrabbing();cam3.MV_CC_StopGrabbing() # #【新增三相机同时关闭并销毁】 cam.MV_CC_CloseDevice();cam2.MV_CC_CloseDevice();cam3.MV_CC_CloseDevice() cam.MV_CC_DestroyHandle();cam2.MV_CC_DestroyHandle();cam3.MV_CC_DestroyHandle() # MvCamera.MV_CC_Finalize()# ch:反初始化SDK---## 5. FAQ| 问题 | 解决思路 ||---|---|| **枚举不到 USB 相机** | 确认已安装 MVS 4.5.1并在“设备管理器”里能看到 Hikrobot Industrial Camera。 || **回调不触发** | 先确认相机真的被外部触发MVS 客户端里能看到帧计数再检查 TriggerSource 是否和硬件接线一致。 || **串口发字符乱码** | ser.write(M.encode(utf-8)) 用 utf-8 编码接收端也请用相同编码。 || **退出时卡死** | 先 StopGrabbing 再 CloseDevice顺序反了会阻塞。 |---## 6. 下一步可以做什么- 把 M/m/O 换成 JSON带上时间戳方便上位机解析。- 用 asyncio pyserial-asyncio 做异步串口减少阻塞。- 把触发信号改成软触发用 Python 直接 MV_CC_SetCommandValue(TriggerSoftware) 做时序同步。--- 如果文章帮到了你欢迎点个 ⭐Star 或留言交流 也欢迎把需求扔给我。
RELATED

相关推荐

按下 Cmd+M 却没反应?这个开源客户端用一个快捷键改动,让 Mac 用户用得省心

按下 Cmd+M 却没反应?这个开源客户端用一个快捷键改动,让 Mac 用户用得省心

按下 CmdM 却没反应?这个开源客户端用一个快捷键改动,让 Mac 用户用得省心 【免费下载链接】ArmCord Legcord is a custom client designed to enhance your Discord experience while keeping everything lightweight. 项目地址: https://gitcode.com…

📅 2026/10/4 10:06:56
AZ-400管道风格对比:YAML管道vs经典管道、托管代理vs自托管代理,到底怎么选?

AZ-400管道风格对比:YAML管道vs经典管道、托管代理vs自托管代理,到底怎么选?

AZ-400管道风格对比:YAML管道vs经典管道、托管代理vs自托管代理,到底怎么选? 【免费下载链接】AZ400-DesigningandImplementingMicrosoftDevOpsSolutions AZ-400 Course Repository for Labs and Demos. 项目地址: https://gitcode.com/gh_…

📅 2026/10/5 21:36:51
Simple Live 跨平台直播聚合工具快速上手指南

Simple Live 跨平台直播聚合工具快速上手指南

Simple Live 跨平台直播聚合工具快速上手指南 【免费下载链接】dart_simple_live 简简单单的看直播 项目地址: https://gitcode.com/GitHub_Trending/da/dart_simple_live Simple Live 是一款完全免费的跨平台直播聚合工具,它把虎牙、斗鱼、哔哩哔哩、抖音四…

📅 2026/10/5 21:42:09
MORE NEWS

更多资讯

📰

BLE产品RF测试实战:Nordic芯片从研发到量产的关键指标

很多人做BLE产品研发的时候,都会有这种阶段:固件跑通了,手机连得上,数据传输正常,于是潜意识里觉得RF这一块“应该没什么问题”。等产品真正进了产线,或者准备送认证的时候才发现,同一批板子的射…

📰

从 ponytail 技能包看大模型技能机制:安装、配置与调试

1. 先搞清楚:你手里这个叫 ponytail 的东西到底是什么我第一次看到 ponytail 这个词,是在同事丢过来的一个链接里。点开是个 Git 仓库,名字就叫 ponytail,没有 README 里的那种花哨介绍,只有一个目录结构:S…

📰

Claude Code全栈AI应用实战:从安装到项目落地

先聊聊我为什么盯上了 Claude Code这段时间我一直在拿 Claude Code 做真实项目,最大感受就是——它真的像个坐在你旁边的高级工程师,而不是一个只会补全代码的智能输入法。市面上 AI 编程工具不少,但 Claude Code 选了一条完全不同的路&#…

📰

AI营销技能库marketingskills:用Claude Code实现SEO、CRO与Analytics自动化

1. 从“marketingskills”说起:一个被低估的AI营销技能库第一次看到marketingskills这个词,是在翻 Claude Code 相关生态项目的时候。当时我的第一反应是:这不就是把营销话术塞给 AI 让它写文案吗?但真正把仓库拉下来跑了一遍之后…

📰

WorkBuddy多机同步实践:符号链接与跨机双写,让AI记忆和规则跟随账号走

1. 为什么单账号多端用着用着就“人格分裂”了如果你也在公司一台电脑、家里一台电脑之间反复横跳,大概会懂这种感觉:白天在办公室给 WorkBuddy 调好的一套规则、顺手教给它的几个技能,晚上回家登录同一个账号,它愣是一脸陌生。对…

📰

嵌入式软件架构规范之 - 分层设计

一、规范的核心思想:驱动文件的“独立性”与“复用性” 该规范的本质是通过分层隔离,实现驱动代码的高复用性、低耦合性,确保驱动模块仅关注“硬件操作逻辑”,不依赖上层业务或下层硬件接口的具体实现细节。其核心要求包括: 二、关键要点解析 1. 驱动文件不能包含应用层…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬