简介:这是一份面向工业自动化方向开发者与VB.NET初学者的OPC通信入门示例,围绕在VB.NET项目中接入OPC服务器展开,可用于理解客户端创建、服务器连接、数据项浏览、读写操作及数据变化事件处理等基础流程。压缩包共38个文件,约194KB,包含VB.NET项目与解决方案文件、核心.vb源代码、Interop.OPCAutomation.dll互操作库,以及exe、pdb、resx、xml等编译与资源配置文件,目录中另有升级报告和说明文档,便于对照项目结构学习。已有198人学习,适合用作VB.NET与OPC技术集成的起步案例,也可作为二次开发时排查引用配置、连接方式和事件注册思路的参考底稿。
1. 从一台只能读数的老上位机说起:VB.NET OPC 示例解决了什么
车间里那台跑了七八年的上位机,屏幕还在,源码没了。产线要接新系统,设备侧只留了一个 OPC DA 服务端,ProgID 还有几个人记得,接口文档一张都没有。手上能用的技术栈是 VB.NET,要做的事其实就四件:连上去、把标签名翻出来、读到数、断线别崩。
这份 Simple VB .NET OPC Example 就是围着这四件事搭的最小可运行集合,走的是 OPCAutomation 这个 COM 包装器,不依赖任何商业控件,一个.vbproj加十几个文件就能跑。适合两类人:接手老产线、必须用 .NET Framework 读 DA 数据的维护工程师;以及想搞明白 OPC DA 的会话、组、项三层模型到底怎么落地的开发者。它不解决 OPC UA,也解决不了跨网段,DCOM 那一段该配还得配。
2. 拆开三层结构:包装器、会话对象、事件回调
2.1 OPCAutomation.dll 是什么,为什么 VB.NET 绕不开它
OPC DA 规范定义的是一组 COM 自定义接口,方法表是编译期定死的,VB.NET 那种后期绑定打不进去。真正让 VB.NET 能写起来的是自动化包装器OPCAutomation.dll:它把自定义接口重新包装成IDispatch可调用的双接口,方法参数全部退化成Object、Array、ByRef。代价是失去强类型,好处是任何支持 COM 的语言都能用,VB6、VBScript、VB.NET 一视同仁。
这里有第一个高频混淆点:OPC.Automation是包装器自己的 ProgID,注册在HKCR\OPC.Automation下;而你要连的某厂商标识.服务器名是服务端的 ProgID,注册在HKCR\CLSID下面另一套键里。工程里New OPCServer()创建的是包装器对象,Connect(progId)里的字符串才是服务端。很多人把这两个字符串搞混,然后对着"类未注册"的报错查一下午。
工程里添加引用时有一点必须手动改:把Interop.OPCAutomation引用的"嵌入互操作类型"(Embed Interop Types)设为False。默认是True,编译能过,运行时事件绑定和ByRef回填会以各种莫名其妙的形式失败,而且报错信息完全不指向真正原因。
选型上也说清楚:如果设备侧已经支持 OPC UA,就别用这套了,UA 的 .NET 标准栈是纯托管、跨平台、不碰 DCOM,一条Session.Create就完事。这套示例存在的唯一理由是——现场只有 DA,且短期内不会改。
2.2 示例工程的对象树:OPCServer → OPCGroups → OPCGroup → OPCItems
示例里所有代码都是围绕下面这棵树展开的,先把对象职责记牢,后面的报错才好定位。
| 对象 | 关键成员 | 职责与注意点 |
|---|---|---|
OPCServer | Connect/GetOPCServers/ServerState/CreateBrowser | 一个进程内建议只建一个实例,代表一条到服务端的会话 |
OPCGroups | Add/DefaultGroupUpdateRate/DefaultGroupIsActive | 组的模板容器,Add()之前设的默认值才会生效 |
OPCGroup | UpdateRate/Deadband/IsSubscribed/DataChange | 更新率与死区的真正载体,一个组对应服务端一条订阅 |
OPCItems | AddItem/SyncRead/SyncWrite/Remove | 只存句柄,不做缓存;句柄失效后必须重新 Add |
OPCBrowser | ShowBranches/ShowLeafs/MoveToRoot | 浏览状态是有记忆的,用完不复位就会"越浏览越深" |
两个句柄概念要分清:ServerHandle由服务端分配,读写全靠它;ClientHandle由客户端自己编号,回调里靠它对号入座。示例里用Dictionary(Of String, Integer)维护 ItemID 到服务端句柄的映射,用自增整数做客户端句柄,这是最省事也最不容易错的做法。
组(Group)不是可有可无的容器。更新率、死区、激活状态、订阅开关都挂在组上,服务端按组为单位组织数据上报。把 500 个点全塞进一个组、更新率设成 100ms,服务端会被你拖垮;按业务分三四个组,各给各的节奏,才是正常用法。
2.3 环境准备:注册、引用、平台目标三件事一次说清
先确认包装器在本机注册过,注意 32 位和 64 位是两份独立注册,缺一份对应位宽的进程就起不来。
# 1) 查一下包装器有没有注册(有输出说明注册过) reg query "HKCR\OPC.Automation" /s # 2) 32 位进程用的那一份,注意 SysWOW64 目录 C:\Windows\SysWOW64\regsvr32.exe "C:\Program Files (x86)\Common Files\OPC\OPCAutomation\OPCAutomation.dll" # 3) 64 位进程用的那一份,在 System32 目录 C:\Windows\System32\regsvr32.exe "C:\Program Files\Common Files\OPC\OPCAutomation\OPCAutomation.dll" # 4) 版本冲突时先反注册旧版再装,别直接覆盖 C:\Windows\SysWOW64\regsvr32.exe /u "旧的\OPCAutomation.dll"工程文件里的三个设置决定了能不能跑起来,少一个都不行:
<PropertyGroup> <!-- 包装器是 COM 组件,走 .NET Framework 最省事,别硬上 .NET 8 --> <TargetFrameworkVersion>v4.8</TargetFrameworkVersion> <!-- 关键:位宽必须和服务端一致,现场 32 位 DA 服务端占多数 --> <PlatformTarget>x86</PlatformTarget> <!-- 必须关掉,否则类型被内嵌,事件绑定和 ByRef 回填会静默失败 --> <EmbedInteropTypes>False</EmbedInteropTypes> </PropertyGroup>三件事的逻辑关系:注册决定"COM 能不能被创建",位宽决定"创建的是哪一份",EmbedInteropTypes决定"引用生成的是互操作程序集还是内嵌类型"。前两个任意一个不对,症状都是0x80040154(类未注册)或0x8007007E(找不到模块);第三个不对,症状是编译通过、运行时事件永远不触发。
还有一条容易漏:入口线程必须标STAThread。VB.NET 的 WinForms 模板默认带<STAThread()>,但如果你把逻辑挪进控制台程序或服务,忘了加这个特性,COM 会在 MTA 里初始化,某些服务端的回调直接静默丢弃。
<STAThread()> Sub Main() Application.EnableVisualStyles() Application.Run(New MainForm()) End Sub3. 从连上到读到数:连接、浏览、同步读写的完整代码
3.1 枚举服务器并建立会话
第一步不是硬编码 ProgID,而是先把本机注册的服务端列出来。GetOPCServers返回的是 1 基数组,这点跟 .NET 习惯相反,用GetValue(i)按下标 1 开始取才不会漏。
Imports OPCAutomation Public Class OpcSession Private _server As OPCServer Private _groups As OPCGroups Private _group As OPCGroup ' 列出本机已注册的 DA 服务端 ProgID Public Function ListServers() As List(Of String) Dim probe As New OPCServer() Dim node As Object = Nothing ' 传 Nothing 表示本机 Dim names As Object = Nothing probe.GetOPCServers(node, names) ' 两个参数都是 ByRef Dim result As New List(Of String) Dim arr As Array = CType(names, Array) For i As Integer = 1 To arr.Length ' 1 基数组,从 1 开始 result.Add(CStr(arr.GetValue(i))) Next Return result End Function ' 建会话并准备一个默认组 Public Sub Connect(progId As String) _server = New OPCServer() _server.Connect(progId) ' 失败抛 COMException,错误码见第 5 章 _groups = _server.OPCGroups _groups.DefaultGroupIsActive = True ' 组必须激活,否则加了项也读不到 _groups.DefaultGroupUpdateRate = 500 ' 毫秒,订阅用 _groups.DefaultGroupDeadband = 0 ' 百分比,0 表示一点变化就上报 _group = _groups.Add() ' 默认值在这一刻被复制进组 End Sub End ClassConnect是同步阻塞的,服务端没响应时会卡到 DCOM 自己的超时(默认可能长达两分钟)。现场调试阶段建议把这一步丢进Task里跑,界面上给个"连接中",不然窗体直接假死,用户以为程序崩了。DefaultGroupUpdateRate单位是毫秒,写 500 就是 0.5 秒一次;这个值在组创建后再改_group.UpdateRate才生效,改OPCGroups上的默认值对已存在的组无效。
3.2 用 Browse 拿到 ItemID,别手抄标签名
标签名(ItemID)是服务端自己定的字符串,可能是通道1.设备1.温度,也可能是Device1\Tag001,没有统一规则。靠在组态软件里抄一遍再粘进代码,是长期维护灾难的起点——设备改名、加了一台就从根上错。正确做法是浏览出来,落库或者写配置文件。
Public Function BrowseAllItems() As List(Of String) Dim result As New List(Of String) Dim browser As OPCBrowser = _server.CreateBrowser() ' 先看第一层分支,用来确认树结构 browser.ShowBranches() For i As Integer = 1 To browser.Count Console.WriteLine("分支: " & CStr(browser.Item(i))) Next ' True = 递归展开到底层叶子,一次列全 browser.ShowLeafs(True) For i As Integer = 1 To browser.Count result.Add(CStr(browser.Item(i))) ' 这个字符串就是要传给 AddItem 的 ItemID Next browser.MoveToRoot() ' 必须复位,否则下次浏览从当前位置开始 Return result End FunctionShowBranches和ShowLeafs改的是浏览器内部的当前节点,不是立刻返回数组。写循环往下钻的时候,每钻一层都要调一次对应方法,靠MoveUp/MoveTo控制位置;钻完不回MoveToRoot,第二次浏览拿到的结果集就跟第一次不一样,这个坑很隐蔽,症状是"同一个程序跑两遍标签数量不一样"。
ShowLeafs(True)在大服务端上可能要几秒到几十秒,别放在窗体Load里同步执行。实际项目里我一般做成"抓取标签"按钮,跑完把结果写进一张本地表,之后只按 ItemID 精确AddItem,浏览动作一个月跑一次就够。
3.3 同步读与批量写:句柄、质量位、时间戳
同步读写的核心是句柄数组。批量读 50 个点,传 50 个服务端句柄进去,一次 RPC 回来 50 个值,比循环调 50 次快一个数量级。
Private _handles As New Dictionary(Of String, Integer) ' ItemID -> 服务端句柄 Private _cliMap As New Dictionary(Of Integer, String) ' 客户端句柄 -> ItemID Public Sub AddTags(itemIds As IEnumerable(Of String)) Dim items As OPCItems = _group.OPCItems For Each id As String In itemIds Dim srvHandle As Integer = 0 Dim cliHandle As Integer = _cliMap.Count + 1 ' 自己编号,回调靠它认点 items.AddItem(id, cliHandle, srvHandle) ' 第三个参数 ByRef 回填 _handles(id) = srvHandle _cliMap(cliHandle) = id Next _group.IsSubscribed = True ' 不订阅也要设,部分服务端依赖它 End Sub ' 单点读,带品质位判断 Public Function ReadOne(itemId As String) As Single Dim items As OPCItems = _group.OPCItems Dim handles(0) As Integer handles(0) = _handles(itemId) Dim values As Object = Nothing Dim errors As Object = Nothing Dim quals As Object = Nothing Dim stamps As Object = Nothing items.SyncRead(OPCDataSource.OPCDevice, 1, handles, values, errors, quals, stamps) ' errors 里非 0 表示这个点失败,但方法本身不会抛 Dim err As Integer = CInt(CType(errors, Array).GetValue(0)) If err <> 0 Then Throw New InvalidOperationException("读失败,错误码 " & err) Dim q As Short = CShort(CType(quals, Array).GetValue(0)) If q <> 192 Then ' 192 = Good Throw New InvalidOperationException("品质位异常: " & q) End If Return Convert.ToSingle(CType(values, Array).GetValue(0)) End Function ' 批量写:值数组必须是 Object(),塞 Integer 会类型不匹配 Public Sub WriteMany(ids As String(), vals As Object()) Dim items As OPCItems = _group.OPCItems Dim handles(ids.Length - 1) As Integer Dim errs As Object = Nothing For i As Integer = 0 To ids.Length - 1 handles(i) = _handles(ids(i)) Next items.SyncWrite(ids.Length, handles, vals, errs) End Sub几个必须知道的细节。SyncRead有两个数据源:OPCDevice直读设备,OPCCache读服务端缓存。调试期用缓存快,但对账、报警判定必须用OPCDevice,否则拿到的是服务端上一次扫描周期的旧值,时间戳还骗你。质量位只认 192 不认别的,64 是 Uncertain、0 是 Bad,实际现场"读到数了但全是 0"十有八九是品质位 Bad 被当成数值用了。时间戳是服务端本地时间,跨时区部署时记得在展示层统一换算。批量写里Object()这个类型不能省,VB.NET 把Double()传进去会在 COM 封送阶段抛类型不匹配,报错信息指向SyncWrite但根本原因是数组元素类型。
3.4 一个能扛住服务端重启的连接骨架
DA 连接不是建一次管一辈子。服务端重启、网线抖动、DCOM 会话超时,表现都是下一次调用抛0x800706BA。示例里给的骨架是"探活 + 重建 + 重新加项",缺了最后一步就白重连了。
Public Function EnsureConnected(progId As String) As Boolean For attempt As Integer = 1 To 3 Try If _server Is Nothing Then Connect(progId) Else If _server.ServerState <> OPCServerState.OPCRunning Then Throw New InvalidOperationException("服务端状态异常") End If _server.GetStatus() ' 便宜的探活,断了这里就抛 If _group Is Nothing Then Connect(progId) End If Return True Catch ex As Runtime.InteropServices.COMException ' 0x800706BA / 0x800706BE:会话已断,整体重建 DisconnectAll() Threading.Thread.Sleep(1000 * attempt) ' 退避,别死循环打服务端 Catch ex As InvalidOperationException DisconnectAll() Threading.Thread.Sleep(1000 * attempt) End Try Next Return False End Function Private Sub DisconnectAll() Try If _group IsNot Nothing Then _groups.Remove(_group.ServerHandle) Catch End Try Try If _server IsNot Nothing Then _server.Disconnect() Catch End Try _group = Nothing _server = Nothing _handles.Clear() ' 旧句柄全部作废,必须重新 AddItem _cliMap.Clear() End Sub重连之后一定要重新AddTags,因为服务端句柄在会话重建后全部失效,直接拿旧句柄去读,返回的是错误码而不是异常,特别容易被忽略,等到上位机显示的全是 0 才发现。退避时间按 1s、2s、3s 递增就行,别写成While True里裸Sleep(100),服务端起来了但被你重连风暴按住的情况真实发生过。
4. 订阅比轮询省事:DataChange 事件、更新率与死区
4.1 轮询和订阅的差别在哪
轮询是客户端定时SyncRead,问一次答一次;订阅是客户端告诉服务端"这些点变了就告诉我",服务端按自己的扫描周期比对,只在变化超过死区时回调。差别在两点:一是网络往返次数,500 个点 1 秒轮询一次,一天就是四万多次 RPC;二是数据新鲜度,轮询永远滞后一个周期,订阅是变化即推。
现场常见误用是把轮询周期设成 100ms 想"逼近实时",结果 CPU 单核跑满、服务端日志刷屏,数据反而更不准。正确姿势是:需要实时响应的点(报警、联锁状态)走订阅、更新率 200~500ms;需要定时对账的点(累计量、班次产量)走轮询,周期按业务定,10 秒、1 分钟都正常。
4.2 挂一个能跑的 DataChange 回调
OPCGroup的DataChange事件必须先声明WithEvents,字段声明和赋值分开写也没问题。
Private WithEvents _group As OPCGroup Private _sink As Action(Of String, Object, Short, Date) Public Sub StartSubscription(updateRate As Integer, deadband As Single, sink As Action(Of String, Object, Short, Date)) _sink = sink _group.UpdateRate = updateRate ' 毫秒 _group.Deadband = deadband ' 百分比,0~100 _group.IsSubscribed = True _group.IsActive = True End Sub ' 注意事件签名:所有数据参数都是 Object / Array / ByRef Private Sub _group_DataChange(TransactionID As Integer, NumItems As Integer, ByRef ClientHandles As Object, ByRef ItemValues As Object, ByRef Qualities As Object, ByRef TimeStamps As Object) _ Handles _group.DataChange Dim ch As Array = CType(ClientHandles, Array) Dim vv As Array = CType(ItemValues, Array) Dim qq As Array = CType(Qualities, Array) Dim tt As Array = CType(TimeStamps, Array) For i As Integer = 1 To NumItems ' COM 回来的数组是 1 基的 Dim cli As Integer = CInt(ch.GetValue(i)) Dim q As Short = CShort(qq.GetValue(i)) Dim name As String = _cliMap(cli) Dim ts As Date = CDate(tt.GetValue(i)) _sink(name, vv.GetValue(i), q, ts) ' 交出去,回调里不做重活 Next End Sub回调线程是 COM 提供的线程池线程,不是你的 UI 线程,也不是主线程。这意味着两件事:第一,绝对不能在回调里直接碰控件;第二,回调里做耗时操作(写数据库、发 HTTP)会把服务端的上报队列堵住,表现是"订阅越来越慢,最后干脆不推了"。我的习惯是回调里只做一件事——把数据塞进ConcurrentQueue,另起一个消费线程批量落库。
4.3 UpdateRate、Deadband、PercentDeadband 怎么定
| 参数 | 单位 | 含义 | 现场经验值 |
|---|---|---|---|
UpdateRate | 毫秒 | 服务端扫描周期,实际值可能被服务端就近取整 | 报警点 200~500,工艺量 1000 |
Deadband | 百分比 | 相对量程的变化阈值,变化小于它不上报 | 0.5~2.0,模拟量建议 1.0 |
PercentDeadband | 布尔 | 死区按百分比还是绝对值解释 | 不确定量程时用True |
IsActive | 布尔 | 组是否参与扫描 | 必须True,否则永远不回调 |
IsSubscribed | 布尔 | 是否向客户端推DataChange | 必须True,只读不订阅时也无害 |
UpdateRate填 0 的意思是"用服务端默认值",不是"越快越好"。有些服务端的最小周期是 100ms,你填 50 它会就近到 100,但如果你填 1000 而它只支持 500,有的实现会抛OPC_E_INVALIDRATE,有的默默取整,两边行为不一致。保守做法是第一次连接后把实际值读回来(_group.UpdateRate),跟配置比对,不一致就写日志。
死区设 0 看着"最灵敏",实际是灾难:一个温度点在噪声下每次扫描都在跳 0.01,订阅就变成 1 秒一次的洪水。按量程算,量程 0~200 度、允许 0.5 度不报,Deadband就是 0.25。字符串和布尔类型的点死区无效,服务端只在值真正变化时才报。
4.4 回调线程不是 UI 线程:跨线程与队列
大多数翻车都发生在这里:调试时好使,一挂上真实数据就抛InvalidOperationException: 线程间操作无效,或者更糟——不抛异常,界面偶尔卡一下,查不出原因。
' 窗体侧:把订阅线程的数据搬回 UI 线程 Private _queue As New Concurrent.ConcurrentQueue(Of (Name As String, Val As Object)) Private Sub OnTagChanged(name As String, val As Object, q As Short, ts As Date) If q <> 192 Then Return ' Bad / Uncertain 先攒着,别直接上屏 _queue.Enqueue((name, val)) End Sub ' 一个 200ms 的 UI 定时器,批量刷,不要在回调里逐条 BeginInvoke Private Sub Timer_Tick(sender As Object, e As EventArgs) Handles Timer.Tick Dim item As (Name As String, Val As Object) Dim count As Integer = 0 While count < 200 AndAlso _queue.TryDequeue(item) dgvTags.Rows(item.Name).Cells("值").Value = item.Val count += 1 End While End Sub为什么不用BeginInvoke逐条刷:一个 500 点的组、1 秒周期,就是 1 秒 500 次BeginInvoke,消息泵被打满,界面比不刷新还卡。攒批 + 定时器是标准解法,200ms 一批,人眼完全看不出延迟。另外DataChange里的Qualities一定要判,Bad 值上屏等于给操作工喂假数据,现场因为一个坏点导致误判的事故并不少见。
5. 避坑:VB.NET 接 OPC DA 的五个高频翻车点
5.1 报"类未注册"或"检索 COM 类工厂失败"
现象:New OPCServer()就抛0x80040154,或者引用能加、编译能过,一运行就报类工厂失败0x8007007E。
原因:两种可能。一是包装器根本没注册,或者只注册了 32 位、你的进程是 64 位(AnyCPU在 64 位系统上默认跑 64 位);二是注册了但 DLL 路径被挪走,注册表里留的是失效路径。
解决:先reg query "HKCR\OPC.Automation" /s看有没有键,再看InprocServer32指向的路径是否真实存在。存在路径问题的,反注册后重新注册。工程把PlatformTarget显式写成x86(大部分现场服务端是 32 位),别用AnyCPU。改完必须清理bin目录重新生成,旧的可执行文件还在以 64 位跑的情况我见过不止一次。
5.2 连接成功但读出来全是 Bad 或全是 0
现象:Connect没报错,AddItem也没报错,读回来的值全是 0,品质位 0 或 64。
原因:ItemID 拼错了。AddItem对不存在的 ItemID 的处理是宽容的——它先把项加进去,返回一个无效句柄,错误要等你读的时候才在Errors数组里体现。另一个可能是数据源用了OPCCache,而组还没激活,缓存是空的。
解决:AddItem之后立刻做一次单点SyncRead(OPCDevice),检查Errors和品质位,把失败的点单独记日志并标红。ItemID 一定要从Browse结果里取,不要手抄;配置里存两份,一份是浏览出来的原始名,一份是业务别名,方便对账。组激活那句DefaultGroupIsActive = True得在Add()之前设。
5.3 程序跑一会儿就 RPC 服务器不可用
现象:连续跑几小时到几天,突然所有读写抛0x800706BA,重连一下又好了,反复发作。
原因:DCOM 会话有保活超时,客户端长时间不发起调用会被服务端回收;或者服务端自己重启过;又或者网络中间有设备静默断开 TCP。
解决:加心跳,30 秒一次读一个廉价点(或者ServerState+GetStatus()),失败就按第 3.4 节的骨架整体重建,重建后必须重新AddItem。同时把_handles字典清空,否则旧句柄会被当有效值继续用,读回来的是错误码而不是异常,这个问题排查起来最费时间。
5.4 DataChange 不触发,或者只触发一次
现象:IsSubscribed = True设了,组也激活了,回调就是不进来;或者第一帧进来了,后面再怎么变都没动静。
原因:三个常见来源。一是WithEvents忘了加,或者EmbedInteropTypes还是True,事件根本没挂上;二是UpdateRate被设成 0 或超过服务端上限,服务端就近取整到一个极长的周期;三是死区设得太大,值确实变了但没超过阈值。
解决:先确认字段声明带WithEvents,工程里那个"嵌入互操作类型"是False。再把Deadband临时设 0、UpdateRate设 500 做一次验证,能触发再往回调参数。如果还是不动,用服务端自带的客户端工具连同一个点看有没有推送,把问题切到服务端侧或 DCOM 侧,别在代码里死磕。
5.5 32/64 位混用与系统安全策略收紧
现象:本机开发一切正常,部署到现场机器就报拒绝访问0x80070005,或者连接直接超时。
原因:开发机和服务端在同一台,走的是本地 COM 调用;现场是跨机器,DCOM 的启动权限、访问权限、标识配置一项没配就过不去。另外新版本系统对匿名远程调用默认更严,旧方案里的宽松配置不一定还生效。
解决:先在现场机器上用服务端自带的客户端工具连一次,确认服务端本身没问题;再按"同一账号、同一工作组、明确的启动与访问权限"这套最小配置逐项对齐。别指望一次全通,我一般会准备一份配置清单逐条勾。极端情况下把客户端和服务端放到同一台机器上跑,先把业务逻辑验证完,再回头啃跨机通信,这样能把问题域缩到最小。
6. 把示例改成能上现场的小工具:心跳、日志与迁移判断
示例跑通只是起点,真正放到产线上还得补三块:心跳、日志、边界判断。
心跳不要只写While True : Sleep。一个可用的做法是维护一张内存里的状态表,每个周期更新一次,界面和外部监控都读这张表:
| 字段 | 类型 | 说明 |
|---|---|---|
LastReadOk | Date | 最近一次成功读到数据的时刻 |
LastWriteOk | Date | 最近一次成功写入的时刻,只读场景可空 |
ReconnectCount | Integer | 累计重连次数,一天超过 5 次就该查网络 |
BadTagCount | Integer | 当前品质位非 Good 的点数 |
ServerState | String | 服务端状态字符串,直接落库 |
日志按"变化才记"的原则写:值变化超过死区记一条、品质位翻转记一条、连接状态变化记一条。全量记日志一天能干出几个 G,而且真出事时翻不动。日志表加一个Source字段区分"订阅上报"和"轮询读取",对账时非常有用。
写入这条线要格外克制。示例里的SyncWrite是没有二次确认的,现场写一个设定值,服务端可能因为设备离线而返回错误码但值已经进了缓存。我的做法是写完 200ms 后回读一次,比对不上就重试一次,还不行就报警置位。写操作永远走白名单,ItemID 不在配置表里的一律拒绝,这条规矩救过我好几次。
至于要不要迁移到 OPC UA:判断标准不是"新不新",而是"设备侧有没有"。设备侧已经暴露 UA 端点、且客户端不受 DCOM 约束,那就该迁,UA 的安全模型、跨平台、无 DCOM 这些优势都是实打实的;设备侧只有 DA、且还有三五年寿命,硬迁的收益抵不上重写和回归测试的成本。折中方案是在中间加一层网关,把 DA 转成 UA,客户端侧代码一次写好,后面换设备不用动。
最后说个习惯。这个示例我第一次跑通的时候,把UpdateRate设成 100、死区设 0,然后在 500 个点的组上看着界面一秒刷十几次,觉得"性能真好"。上线第二天服务端 CPU 打满,整条线的数据全挂。从那以后我每次接新的 DA 服务端,都强制走一遍固定流程:先浏览标签落表,再单点验证品质位,然后小批量(不超过 20 个点)试订阅,最后才按业务分组放大,UpdateRate和死区一律从保守值往上调。这套顺序看着慢,实际上省掉的排查时间远多于它花掉的。希望帮到你。
本文还有配套的精品资源,点击获取