简介:C#与VisionPro联合开发的项目工程资源,面向需要将康耐视工业视觉功能集成到上位机应用中的工程师与自动化开发者。内容围绕实际现场试用场景,演示如何通过C#窗体界面调用VisionPro的视觉检测、图像定位与识别能力,其中包含主窗体设计、感兴趣区域参数设置以及输入输出交互等关键模块的完整实现,适合有一定C#基础并希望上手机器视觉二次开发的读者参考。压缩包共一百五十五个文件,大小约五十兆字节。工程以C#源码、程序集和界面布局文件为核心,同时配有视觉工具文件、资源文件、配置文件及运行说明文档,目录结构清晰,便于导入学习或在已有基础上修改复用。目前已有二百二十六人浏览学习。压缩包内提供了从界面到视觉逻辑的完整工程样例,可查看窗体设计代码、视觉流程配置与项目文件结构,理解VisionPro与C#通信集成时的关键处理细节,涵盖视觉工具参数配置、输入输出信号联动与异常排查思路,并参考现场试用沉淀的调试经验,减少自行摸索成本。
1. C# 与 VisionPro 联合开发:现场跑得稳的程序,从来不靠拖控件
C# 与 VisionPro 联合开发,听起来就是把 QuickBuild 里调好的工具拖进 WinForms,实际在现场跑过两三个星期的人不会这么想。这套源码在产线上一直试用,主窗体、ROI 参数表单、I/O 映射表单各管一块:C# 负责流程、界面和通信,VisionPro 负责图像定位、检测与识别,两边靠 ToolBlock 的输入输出对接。项目里除了 .csproj,还有一堆 .cache、app.config、.exe.config,哪些要留、哪些删了反而省事,我逐个拆开讲清,再给一条可以照着复现的集成路径。适合正在做视觉上位机、想把 .vpp 方案嵌进自己程序的开发者。
2. 先看清工程骨架:.csproj、app.config 与那堆 .cache 文件的去留
拿到这份资源,第一步不是去拖控件,而是把项目结构里的文件逐个认一遍。VisionProSystem.csproj 是主工程文件,MainForm.Designer.cs 是主窗体设计器生成的代码,ROIParamForm.cs 是参数调整窗体,IOForm.Designer.cs 是 I/O 映射窗体的设计器代码。真正容易让新手翻车的,是根目录下那一排以 .cache 结尾的文件,以及 app.config、.exe.config、.vshost.exe.config 之间的区别。这节先把工程骨架说透,后面再谈怎么跑视觉。
2.1 那堆 .cache 文件是什么,能不能删
资源列出的 DesignTimeResolveAssemblyReferences.cache、ResolveAssemblyReference.cache、DesignTimeResolveAssemblyReferencesInput.cache,是 Visual Studio 在解析程序集引用时生成的缓存文件。每次打开项目、添加或移除引用,VS 都要重新计算哪些 DLL 会被编译进输出目录,计算结果就写进这些 cache。它们不是源码,也不是配置,删掉之后 VS 会在下一次工程加载时按当前引用关系重新生成。唯一需要留意的是,删除之前要把 VS 关掉,否则文件被占用,或者 VS 用旧缓存覆盖新生成的缓存,反而容易出现「添加了引用却不生效」的假象。
VisionProSystem.csproj.GenerateResource.Cache 则是资源编译器生成的缓存,对应项目里的 Properties.Resources.resx。现场经常遇到一个诡异情况:改了资源文件里的图标或字符串,重新编译后界面显示的还是旧内容。很多时候不是代码没改对,而是这个 .cache 没有失效。处理办法就是把它删掉,然后重新生成项目。所以结论很简单:这些 cache 文件全部不用进代码库,.gitignore 里直接忽略,本地删掉也完全不影响编译。如果哪天 VS 解析引用出现莫名其妙的问题,关掉 VS,删掉 obj 目录下的 cache,重新打开工程,能解决大半。
2.2 app.config 与 .exe.config:运行时到底读哪一份
app.config 是设计时配置,编译过后会被复制成输出目录里的 VisionProSystem.exe.config,程序运行时读的是后者。Visual Studio 里改了 app.config 保存后,编译输出会自动同步,但手动往发布目录拷贝文件时很容易只拷 exe 忘了拷 config,或者拷了旧的 config,导致启动后参数和连接串全是旧值。另一个容易忽略的是 VisionProSystem.vshost.exe.config,这是 VS 调试宿主用的配置,只影响本地调试,正式部署时不需要带出去。
VisionPro 的底层程序集并不是纯粹的托管代码,里面混着原生组件。在 .NET Framework 4.x 环境下,如果 app.config 里缺少运行时策略声明,加载某些 VisionPro 模块时会被 CLR 直接拒绝,报错信息还比较隐晦。常见做法是在 app.config 里加上 startup 配置,并打开 useLegacyV2RuntimeActivationPolicy,让混合模式程序集能正确加载:
<?xml version="1.0" encoding="utf-8"?> <configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> </startup> <appSettings> <add key="VppPath" value="config\vision.vpp" /> <add key="OutputPath" value="D:\InspectResult" /> </appSettings> </configuration>这里的 useLegacyV2RuntimeActivationPolicy 是关键参数,如果现场启动时提示无法加载 VisionPro 相关程序集,先检查这一行有没有被误删。supportedRuntime 指定目标框架版本,要与 csproj 里 TargetFrameworkVersion 一致。appSettings 里我习惯把 .vpp 路径和输出目录放进去,因为现场部署时这些路径经常要改,没必要为改一次路径就重新编译一遍。
2.3 引用哪些 VisionPro 程序集:Copy Local 的坑
VisionPro 的程序集是按功能拆分的,不是引用一个 DLL 就万事大吉。定位用 PMAlign,检测用 Blob,读码用 ID,图像预处理还有 ImageProcessing。csproj 里引用哪些程序集,直接决定发布目录里要带哪些 DLL。看这份资源的文件结构,主窗体、ROI 表单、I/O 表单分得比较清楚,对应的引用也相对固定。下面的引用片段是典型的 WinForms 工程写法:
<ItemGroup> <Reference Include="VisionPro.Core"> <HintPath>..\packages\VisionPro\VisionPro.Core.dll</HintPath> <Private>True</Private> </Reference> <Reference Include="VisionPro.PMAlign"> <HintPath>..\packages\VisionPro\VisionPro.PMAlign.dll</HintPath> <Private>True</Private> </Reference> <Reference Include="VisionPro.Blob"> <HintPath>..\packages\VisionPro\VisionPro.Blob.dll</HintPath> <Private>True</Private> </Reference> </ItemGroup>Private 节点对应 Visual Studio 属性面板里的「复制本地」。设成 True,程序集才会被复制到输出目录。现场部署时最常见的报错是「未能加载文件或程序集 VisionPro.Core」,十有八九是某台机器上的发布目录少了 DLL,或者 Private 被设成了 False。我一般会在打包脚本里显式检查输出目录下是否有 VisionPro 相关的全部 DLL,而不是依赖 IDE 的复制行为。
另外要注意,.vpp 文件不算程序集,它是 QuickBuild 保存下来的视觉方案文件,包含了工具链配置和参数。发布时要把 config 目录连同 .vpp 一起拷到 exe 同目录下。第 4 章会讲 C# 里怎么加载它,这里先记住一点:加载路径不要写绝对路径,用 AppDomain.CurrentDomain.BaseDirectory 拼相对路径,否则换一台电脑就找不到文件。
3. 主界面与 ROI 参数表单:现场改参数,不重新编译的交互方式
视觉项目的现场调试,改的最多的往往不是代码,而是检测阈值、匹配分数、角度范围这些参数。如果每次改参数都要打开 Visual Studio 重新编译,现场调试效率会很低。这份资源的工程里,ROIParamForm.cs 的存在就是为了解决这个问题:它和 MainForm 解耦,运行过程中打开参数窗体,改完直接生效。这章讲清楚三个窗体的分工,以及参数从界面到视觉工具链的完整链路。
3.1 三个窗体的分工:MainForm、ROIParamForm、IOForm
MainForm.Designer.cs 是主窗体,负责相机显示、启动停止流程、结果展示,也是整个程序的骨架。ROIParamForm.cs 是参数调整窗体,封装了和视觉工具链相关的可调参数。IOForm.Designer.cs 则是 I/O 映射窗体,现场接线时用来确认哪个输入信号对应哪个输出动作。三个窗体分开设计,而不是全部堆在主窗体里,是因为它们的修改频率完全不同:主窗体逻辑基本稳定,ROI 参数可能一天调好几次,IO 映射只在接线时动。
实际项目中我一般把 ROIParamForm 设计成非模态窗体,主窗体上放一个按钮打开它,这样操作员可以一边看图像结果一边调参数。IOForm 则做成模态窗体,因为 I/O 映射一旦改错会影响设备动作,不能让操作员频繁进入。窗体之间的数据传递不要用全局静态变量,而是通过构造函数传入当前正在运行的 CogToolBlock 实例。下面这段代码是 ROIParamForm 构造时的标准做法:
// 在 MainForm 中实例化参数窗体时,把当前 ToolBlock 传进去 private void btnROI_Click(object sender, EventArgs e) { ROIParamForm frm = new ROIParamForm(this.currentToolBlock); frm.Show(this); }通过构造函数传依赖对象,比设置公共属性更安全,因为参数窗体在没有 ToolBlock 时根本无法工作,构造时就把它定死,避免运行到一半发现 currentToolBlock 是 null。这个习惯对现场调试很重要,因为参数窗体经常被反复打开关闭,生命周期必须清晰。
3.2 从 ToolBlock 到界面控件:参数绑定的实现
ROIParamForm 的职责,是把 ToolBlock 的输入输出绑定到界面控件上。VisionPro 的 CogToolBlock 可以通过名称访问输入输出,这比记住工具内部的对象模型要省事得多。前提是在 QuickBuild 里已经把输入的变量名定义清楚。比如我在 .vpp 里定义了 Threshold、AngleMax 两个输入,C# 侧就这么读:
private void LoadParams(CogToolBlock toolBlock) { if (toolBlock == null) { return; } // 输入集合用名称索引,Value 是 object,必须做类型转换 this.nudThreshold.Value = Convert.ToDecimal(toolBlock.Inputs["Threshold"].Value); this.nudAngleMax.Value = Convert.ToDecimal(toolBlock.Inputs["AngleMax"].Value); }这段代码有两个关键点。一是 Inputs 集合的索引是字符串名称,拼写必须和 QuickBuild 里的变量名完全一致,大小写也要对上,否则运行时会抛异常。二是 Convert.ToDecimal 用于把 VisionPro 里常见的 double 类型转成 NumericUpDown 控件的 decimal 类型。如果转换报错,多半是 .vpp 里那个变量的类型不是数值,检查 QuickBuild 里对应的输入类型。
加载时机也要注意。ROIParamForm 的 Load 事件里调用 LoadParams,而不是构造函数里调用。原因是窗体构造时句柄还没准备好,访问控件属性有时会触发隐式创建句柄,导致第一次显示时闪烁或者数据没刷上去。这个细节在工控机上尤其明显,因为现场电脑显卡驱动和刷新率经常很怪。
3.3 参数热更新:写完输入再 Run,跨线程更新 UI
参数窗体改完数值后,要写回 ToolBlock 并触发重新运行。很多人在这里会犯一个错误:只改了界面控件的值,没有写回 ToolBlock,或者写回了但没有重新 Run,导致界面显示新参数、实际执行结果还是旧参数。正确的执行顺序是先写 Inputs,再调 Run:
private void ApplyParams(CogToolBlock toolBlock) { // 先把界面控件值写回 ToolBlock toolBlock.Inputs["Threshold"].Value = (double)this.nudThreshold.Value; toolBlock.Inputs["AngleMax"].Value = (double)this.nudAngleMax.Value; // 再触发整条工具链重新运行 toolBlock.Run(); // 运行后刷新结果,注意这行不能省略 bool overallPass = Convert.ToBoolean(toolBlock.Outputs["OverallPass"].Value); UpdateStatus(overallPass ? "当前参数:OK" : "当前参数:NG"); }参数写回后,Run 会带动整条工具链重新执行。要注意 ToolBlock.Run 是同步调用,如果图像比较大,耗时可能到几十毫秒甚至上百毫秒。在 UI 线程里直接调用 Run,窗体商会明显卡顿。常见做法是把 Run 放到后台线程,结果再通过 BeginInvoke 回 UI 线程更新:
private void UpdateStatus(string text) { if (this.IsDisposed) { return; } if (this.InvokeRequired) { this.BeginInvoke(new Action<string>(UpdateStatus), text); return; } this.lblStatus.Text = text; }Invoke 和 BeginInvoke 的区别,现场工程师应该很清楚:Invoke 是同步等待 UI 线程执行完才返回,如果在 UI 线程里调用 Invoke 自己等自己,会死锁;BeginInvoke 是异步投递,不会阻塞当前线程。在相机采集回调或后台工作线程里,我一律用 BeginInvoke,并且在方法开头加 IsDisposed 检查,因为窗体关闭后回调仍然可能触发,这时候再 Invoke 会抛 ObjectDisposedException。这是现场程序崩溃的一个高频原因。
3.4 参数持久化:别把现场参数写进 .vpp
改完参数要保存,下次启动还能用。很多初学者会把参数直接写回 .vpp 文件,这个做法在后患很大。.vpp 是 QuickBuild 的视觉方案文件,它被 VisionPro 自己的编辑器改过之后,C# 侧很难对比差异;而且视觉工程师和上位机工程师同时改同一个 .vpp,现场会乱成一锅粥。我比较常用的是把运行态参数单独存成一个 JSON 文件,启动时加载 .vpp 之后再覆盖运行态参数:
// 保存参数到 exe 同目录 string json = JsonConvert.SerializeObject(paramDict, Formatting.Indented); File.WriteAllText( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "param.json"), json, Encoding.UTF8 );启动顺序很关键:先加载 .vpp,再读 param.json 覆盖对应输入,最后才 Run。这样视觉工程师可以用 QuickBuild 调 .vpp 里的默认参数,上位机这边用 param.json 管理现场微调值,两边互不干扰。每台设备的参数差异通过 param.json 隔离,换设备时只需要复制这个文件,不用重新编译程序。
4. 把视觉工具链装进 C#:加载 .vpp、改参数、读结果的实现路径
这章是整个集成方案的核心。VisionPro 工具链不一定非要在 C# 里用代码一个个 new 出来,更常见也更稳的做法是用 QuickBuild 调好视觉方案,存成 .vpp 文件,然后在 C# 里通过 CogJobManager 加载并调用。后一种方式把视觉逻辑和上位机逻辑分开,视觉工程师改工具链不用动 C# 代码,上位机工程师也只需要关心输入输出,是现场长期试用后留下来的方案。
4.1 为什么用 CogJobManager 加载 .vpp,而不是直接 new 工具
理论上可以在 C# 里直接实例化 CogPMAlignTool、CogBlobTool,然后手动配置每一个工具的属性。但这样做的代价是:每改一个视觉参数都要重新编译上位机,视觉方案的可视化调试完全没法做,出了问题也很难定位是哪个工具的参数不对。用 .vpp 文件保存工具链的好处在于,QuickBuild 里能看到图像、改参数、单步运行,确认没问题后保存,C# 侧只需要加载它,然后往 Inputs 里塞图像、从 Outputs 里取结果。
CogJobManager 是 VisionPro 里管理视觉任务的总入口。它可以加载包含了多个 Job 的 .vpp 文件,也可以单独管理一个 ToolBlock。现场大部分产线只需要一个工位一个视觉任务,所以一份 .vpp 里通常就一个 Job。加载代码看起来很简单,但它背后的对象关系决定了后续代码怎么写:
CogJobManager jobManager = new CogJobManager(); // 拼接 .vpp 路径,保证换机器不用改源码 string vppPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "config", "vision.vpp" ); jobManager.Load(vppPath); // 取出第一个 Job,再拿到它内部的 ToolBlock CogJob job = jobManager.Job(0); CogToolBlock toolBlock = job.ToolBlock;. .vpp 路径用 BaseDirectory 拼接,是因为现场部署时程序可能装在 D 盘、E 盘甚至网络路径下,写成绝对路径 C:\Vision\vision.vpp 换台电脑就废。Job(0) 取第一个任务,如果 .vpp 里有多个 Job,需要用任务名精确获取,避免索引错位。ToolBlock 是工具链的容器,所有工具都在它里面串行或并行执行,后续读写参数都通过它,不直接操作单个工具对象。
4.2 在 C# 中修改视觉参数并运行工具链
上一章的参数窗体,最终也是走到这一步。ToolBlock 的 Inputs 集合,等于把 .vpp 里暴露出来的输入变量集中到了一个字典里。运行时修改定位或检测参数,不需要深入工具内部,只要改 Inputs 集合的对应项就行。以定位场景为例,经常调的是匹配分数阈值和角度搜索范围:
// 运行前修改输入参数 toolBlock.Inputs["ScoreThreshold"].Value = 70.0; toolBlock.Inputs["AngleMax"].Value = 30.0; // 把相机图像送入输入 toolBlock.Inputs["InputImage"].Value = currentImage; // 同步执行整条工具链 toolBlock.Run();ScoreThreshold 是匹配分数阈值,VisionPro 里一般是百分制,70 表示匹配度低于 70 分的都判定为找不到目标。AngleMax 是允许的旋转角度范围,单位是度,现场要根据产品摆放的随机性来定,给得太小容易误判 NG,给得太大容易匹配到错误位置。InputImage 是固定的图像输入名,也可以用其他名称,但必须和 .vpp 里工具链的输入端子名称一致。
Run 执行完之后,所有工具的运行状态都已经刷新。这里要提醒的是,ToolBlock.Run 不是线程安全的,同一时刻多个线程同时调用会出现不可预期的结果。现场如果有多个触发源,比如相机硬触发和软件触发同时打开了,就要在 C# 侧加锁或者用状态位保证同一时刻只有一个线程在调 Run。
4.3 读取结果:输出集合、状态判定和结果表的组织
工具链运行完,结果全部在 ToolBlock.Outputs 集合里。出结果之前,最好先确认工具链本身运行没有报错,否则读出来的值没有意义。VisionPro 的每个工具都有 RunStatus,但通过 ToolBlock 的 Outputs 读取时,更常用的做法是在 QuickBuild 里把最终判定结果定义成一个输出变量。比如我在 .vpp 里定义 OverallPass、OffsetX、OffsetY 三个输出:
// 先读运行状态,再读业务结果 bool toolChainOK = toolBlock.RunStatus.RunState == false; // 简化示意 bool overallPass = Convert.ToBoolean(toolBlock.Outputs["OverallPass"].Value); double offsetX = Convert.ToDouble(toolBlock.Outputs["OffsetX"].Value); double offsetY = Convert.ToDouble(toolBlock.Outputs["OffsetY"].Value);RunStatus 的具体判断方式在不同 SDK 版本里有差异,我这里用注释示意,实际开发时以你当前版本对象模型为准。比较规范的做法是先在 QuickBuild 里把「最终 NG 判定」用逻辑工具算好,输出成一个布尔变量,C# 侧只读这个变量,不要在 C# 里再去拼接多个工具的分数做二次判断。这样视觉判定逻辑全部留在视觉侧,上位机只做搬运工,逻辑清晰,排查问题也方便。
结果数据的组织建议用一个自定义类承载,不要用一堆零散的变量到处传。定义如下:
public class InspectResult { public bool Pass { get; set; } public double OffsetX { get; set; } public double OffsetY { get; set; } public string ProductId { get; set; } public DateTime Time { get; set; } }这样一个对象既能绑定到界面 DataGridView,也能直接传给通信层发往 PLC。现场查看历史记录时,按 ProductId 和时间过滤就够了,不用临时拼字符串。
5. 现场联调避坑:5 个只有上过线才会信的坑
桌面 demo 和现场产线的最大差别,在于状态管理和异常恢复。开发机上程序崩了可以重来,现场崩一次可能就是停线,所以下面这五个坑都来自实际联调,每一条都按「现象 → 原因 → 解决」的顺序讲清楚。你会发现大部分问题不是视觉算法本身,而是工程整合层面的细节。
5.1 程序启动即崩,或提示 VisionPro 相关程序集加载失败
现象:双击 exe 后程序一闪而过,Windows 事件查看器里记录到「未能加载文件或程序集」异常;或者程序起来了,但一创建视觉对象就报错。
原因:发布目录缺少 VisionPro 运行时 DLL;或者 csproj 里相关引用设成了不复制到本地;还有一种情况是目标机器没装 VisionPro 运行库,开发机上能跑是因为开发机装了完整 SDK。
解决:把启动逻辑包进 try-catch,把异常信息直接弹出来,而不是让程序无声退出:
try { CogJobManager manager = new CogJobManager(); manager.Load(vppPath); } catch (Exception ex) { MessageBox.Show( "VisionPro 初始化失败,原因:" + ex.Message + "\n请检查运行库和 .vpp 路径。", "初始化错误" ); }这个 try-catch 的意义不只是提示用户,更是给现场工程师一个明确的排查方向。如果异常信息指向某个 VisionPro 程序集加载失败,用依赖检查工具看一眼发布目录,缺哪个 DLL 补哪个。从那以后我每次打包发布都会把整个输出目录做成清单,现场核对时逐个勾,不再信「我本机跑得好好的」这句话。
5.2 内存只涨不降,跑一个班次后程序越来越卡
现象:内存占用从启动时的 300MB 慢慢涨到 1GB、2GB,界面操作变卡,最终可能直接报内存不足。
原因:图像对象没有被及时释放。采集回调里每次触发都创建一个新的图像对象,处理完没有 Dispose,VisionPro 的图像内存又比普通托管对象大得多,几百张图累积起来就是几个 GB。
解决:采集回调里用 using 包裹图像对象,处理完立即释放:
private void OnImageAcquired(object sender, ImageAcquiredEventArgs e) { using (var image = e.Image) { toolBlock.Inputs["InputImage"].Value = image; toolBlock.Run(); ReadResult(); } }using 能保证代码块执行完图像对象被释放,即使 Run 抛异常也会走释放流程。这里的事件参数类名不同相机厂商可能不一样,但图像对象的生命周期管理原则是通用的。如果释放后内存仍然缓慢上涨,用内存分析工具抓一下是否是 VisionPro 内部缓存没有清空,必要时调用 GC.Collect,但不要每帧都调,性能会很难看。
5.3 界面假死:采集回调里直接更新控件
现象:视觉程序正常运行,相机每来一帧图像,界面就卡顿一下;连续触发时整个窗体直接无响应,移动窗口都困难。
原因:采集回调线程直接访问了 UI 控件,或者 ToolBlock.Run 被放在了 UI 线程执行。WinForms 控件不是线程安全的,跨线程更新控件轻则偶发异常,重则把 UI 线程堵死。
解决:把视觉处理放到后台线程,UI 更新统一走 BeginInvoke。前面 3.3 小节的 UpdateStatus 方法就是标准写法。更完整一点,视觉处理本身也要防重入:
private void StartInspect() { Task.Run(() => { if (!Monitor.TryEnter(this.lockObj)) { return; // 上一帧还没算完,丢弃本次触发 } try { toolBlock.Inputs["InputImage"].Value = currentImage; toolBlock.Run(); bool pass = Convert.ToBoolean(toolBlock.Outputs["OverallPass"].Value); this.BeginInvoke(new Action<bool>(UpdateResult), pass); } finally { Monitor.Exit(this.lockObj); } }); }Monitor.TryEnter 这个细节很关键:产线上的相机触发频率可能远高于视觉处理速度,如果每个触发都开一个线程去处理,最终只会累积一堆执行不完的任务然后内存爆炸。TryEnter 拿不到锁就说明上一帧还在处理,直接丢弃这一帧的触发即可。界面假死的坑,本质是线程模型没有从一开始设计清楚,等现场出了问题再缝缝补补,反而更费时间。
5.4 现场改了 ROI 参数,重启程序又变回旧值
现象:操作员在 ROIParamForm 里把检测阈值从 80 改成 60,当时运行效果也确实变了,但重启程序后一切又回到 80,好像从没改过。
原因:参数只写进了运行内存里的 ToolBlock,没有持久化到文件;或者程序启动时加载 .vpp 的动作把之前改过的值全部覆盖了。.vpp 是静态文件,运行时的修改不会自动写回。
解决:按照第 3 章的方案,把运行态参数单独存成 param.json。保存按钮的完整逻辑是:先写回 ToolBlock → 立即 Run 验证 → 验证通过 → 序列化到 JSON 文件。启动逻辑是:加载 .vpp → 读取 param.json → 覆盖 Inputs。这样改参数的人只需要动界面,重启后程序会把上次生效的参数再加载一遍。关键点是「启动覆盖顺序」,顺序错了,保存多少次都没用。
5.5 I/O 信号错位:PLC 收到结果永远慢一拍
现象:设备端动作和视觉结果对不上,PLC 拿到的 OK/NG 信号总是指上一次产品的结果,不良品明明是刚检测出来的,机构却放行了上一个。
原因:视觉处理是异步的,触发信号来了之后程序立刻开始采集和处理图像,但输出信号更新得太早,或者输出信号直接绑定了相机的触发完成信号,没有等待 ToolBlock.Run 结束。IOForm 里如果只做「触发信号 → 输出信号」的直连映射,没有中间的状态锁存,就会发生信号错位。
解决:把视觉流程改成同步闭环,触发到来后先检查是否空闲,然后运行工具链,得到结果后再更新输出信号:
private void OnTriggerReceived() { if (this.isBusy) { // 上一产品还没处理完,丢弃本次触发或标记过载 return; } this.isBusy = true; try { toolBlock.Run(); bool pass = Convert.ToBoolean(toolBlock.Outputs["OverallPass"].Value); // 结果产生后再刷新 DO 点,避免中间态输出旧结果 ioOutput.SetDo("InspectOK", pass); ioOutput.SetDo("InspectNG", !pass); } finally { this.isBusy = false; } }isBusy 状态位是 IO 时序的充血药:不管输出通路多复杂,只要保证「视觉结果没出来之前绝不动输出信号」,信号错位这个坑就不会踩。IOForm 里的映射配置可以作为一张静态表,把 DO 点名称和 PLC 地址一一对应,程序启动时读表初始化,现场换点位只需要改配置文件,不用动代码。
6. 从能跑到跑得稳:现场长期试用的四个验证动作
程序能跑通视觉,距离能在产线上 24 小时连续运行,中间还隔着稳定性验证这一步。这套工程在现场长期试用下来,我总结出四个必做的验证动作,与其说是技术,不如说是纪律。
| 验证动作 | 具体做法 | 通过标准 |
|---|---|---|
| 版本与授权核对 | 启动画面显示 VisionPro 版本号和授权状态 | 与现场验收单一致 |
| 图像与日志落盘 | NG 图按日期存盘,日志记录每次检测结果 | 连续运行 2000 次,图像与日志无丢失 |
| 开机自启与看门狗 | 注册成计划任务或服务,进程退出自动拉起 | 断电重启后程序自动恢复运行 |
| 参数备份恢复 | param.json 每次保存自动复制到 backup 目录 | 现场改错参数后可以从备份回退 |
日志这块常被当成小事,现场排查时却最救命。检测日志至少要包含时间、产品标识、OK/NG 结果、偏移量这几个字段,时间精确到毫秒:
private void WriteLog(string productId, bool pass, double offsetX) { string logPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "logs", DateTime.Now.ToString("yyyyMMdd") + ".txt" ); File.AppendAllText( logPath, $"{DateTime.Now:HH:mm:ss.fff} | {productId} | {(pass ? "OK" : "NG")} | {offsetX:F3}\n" ); }毫秒级别的时间戳,是为了能和相机触发记录对齐,排查「到底哪一帧图像对应哪个结果」时非常有用。产品 ID 作为每一行的主键,后面接任何统计都能对上。日志写完要确认落盘,不要只写进内存缓冲,现场一旦断电,没落盘的日志等于没有。
这套工程我拿到手后,第一个验证动作就是核对 VisionPro 版本和 .vpp 文件格式,版本不一致时 QuickBuild 打开 .vpp 会提示迁移,C# 程序直接加载可能报错。版本对齐之后再看参数文件、IO 映射表、日志目录,全部确认一遍才让程序连相机。这个习惯是某次现场跑了一周后半夜翻车换来的经验,从那以后我每次部署新版本都强制走完这四个动作,再回头调界面样式,一个都不省。希望帮到你。
本文还有配套的精品资源,点击获取