简介:这是面向工业自动化开发者的OPC UA客户端示例,基于Visual Basic(VB)编写,适合正在学习OPC UA通信协议,或需要在VB环境中快速实现数据采集与读写功能的工程师借鉴参考。压缩包大小15.26MB,共181个文件,以122个DLL类库为主,另含VB项目源码、XML配置、EXE可执行程序、PDB调试符号及少量工程缓存文件,其中TestOpcUaVb项目可直接打开调试。资源已有481人学习浏览。代码完整覆盖OPC UA客户端的核心交互流程,包括连接与安全认证、信息模型节点浏览、数据订阅通知、数值读写和异常处理等关键模块;配合配套的OPC Helper 2.02工具,可快速连接真实OPC服务器进行读写验证,理解UA客户端原理。通过研究项目源码,还能掌握VB环境下创建会话、构建订阅、订阅释放等SDK调用方式,为工业系统集成提供可复用的实践模板。1. 这份VB写的OPC UA客户端,能让你少走三个月弯路去年在车间调试一台老设备的联网采集,上位机是VB6改过来的,新设备却只提供OPC UA接口。网上搜了一圈,C#的示例倒是不少,VB的却少得可怜。翻遍几个工控群,最后找到这份用VB写的客户端工程,才把数据采回来。它不是完整的商业软件,只是一个能连接、能读点、能订阅的客户端骨架,代码风格直白,没有多余的封装,非常适合用来学习OPC UA客户端的开发思路。如果你也在做设备数据采集、MES对接、车间看板,又没有现成的C#同事帮你写底层,这份资源值得解压后对照着敲一遍。2. 先看懂OPC UA客户端:通信模型、核心对象和VB选型2.1 客户端面对的不再是DA的Item,而是Endpoint和NodeOPC UA的通信模型比传统OPC DA(那套COM/DCOM的冒号反斜杠)清晰得多。在客户端这边,你不再需要逐个“组态”变量,而是先和服务端建立一条安全通道,再通过节点ID直接操作数据。整个体系里,你只需要盯住几个核心对象:EndpointDescription描述的是服务端暴露的通信地址和安全策略;ApplicationConfiguration描述的是客户端自己的身份、证书和超时参数;Session是双方建立的一条逻辑会话,所有读写都发生在这条会话上;而Node是服务端地址空间里的数据节点,变量、参数、报警都抽象成Node,每个Node有一个唯一的NodeId。我一开始忽略了Node和旧的Item的区别,总想把变量当成独立的东西去配置,后来发现OPC UA更方便:地址空间本身就是一棵树,你想读什么,先通过NodeId定位那个节点,再在会话上操作它就行。选型上,VB环境要用UA库,最常见的选择是直接用官方发布的Ua .NET Standard库,它在VB.NET里可以直接引用,证书验证、会话管理、订阅回调都封装好了,适合把精力放在业务逻辑上。如果你的项目还在用VB6,那就只能走OPC DA兼容层或者另外写COM封装,代价大得多,我建议新项目直接上VB.NET加这个库。顺带说一个容易让人绕晕的点:OPC UA的地址空间是有层级结构的,根节点下面有ObjectsFolder,之后才是设备节点和数据变量。你用浏览工具看到的是一个树,而不是旧OPC DA里那种扁平的Group和Item列表。这个变化对代码的影响在于,你不用维护一个“项集合”,只要记住几个关键NodeId就能随时访问。真正费时间的反而是如何从服务端那棵大树上精准找到你要的变量,这个我会在联调章节里专门讲浏览代码。2.2 用VB引用UA .NET库:项目设置里的三个决定项先讲如何创建工程:新建一个VB项目,通过包管理器安装UaClient相关的包,然后在代码里引用命名空间。这里有三项设置会影响后面所有代码:第一,目标框架。我一般会选.NET Framework 4.7.2或更高,兼容性好,服务端和工具集都不容易出幺蛾子。选到.NET Core也可以,但要注意某些第三方模拟器只支持.NET Framework,会导致联调时来回折腾。第二,平台目标。如果你的项目默认是AnyCPU,在连接本地服务端时没问题,但一旦用到证书和某些本地库,偶尔会报BadImageFormatException。常见做法是把“平台目标”固定为x64,尤其是Windows 10以后的系统,对应服务端多为64位进程。第三,应用配置里ApplicationName和ApplicationUri。服务端在证书验证阶段会查看客户端证书的应用URI,如果证书和配置不一致,会直接拒绝连接。学习阶段可以先关闭证书验证,但你要知道这个坑存在。下面是一个最小配置的代码示例,放在Form的构造函数或Module启动段里就能用:' 创建客户端ApplicationConfiguration,注意证书路径可以暂时留空 Dim config As New ApplicationConfiguration() config.ApplicationName = "VB OPC UA Client" config.ApplicationUri = "urn:vb:opcuaclient" config.ApplicationType = ApplicationType.Client ' 学习阶段先自动接受未信任证书,生产环境必须改为False config.SecurityConfiguration = New SecurityConfiguration() config.SecurityConfiguration.AutoAcceptUntrustedCertificates = True ' 输出日志到控制台,方便排查连接问题 config.SetTraceMask(Utils.TraceMasks.StackTrace)这段配置里的ApplicationName要和你后面创建的证书匹配,否则服务端会回一个BadCertificateInvalid。ApplicationUri是全局唯一标识,建议用urn:开头的固定字符串,不要每次启动都随机生成,否则服务端会认为你在反复换身份。AutoAcceptUntrustedCertificates这个开关是学习时偷懒用的,放在车间现场必须关掉,不然后面证书问题会被人盯上。还有一点容易忽略:Ua .NET Standard库的大部分API是异步的,VB里要用Async/Await或者把返回的Task直接阻塞等待。我见过不少同事直接写Session.Create(...).Result,结果在UI线程上死锁。后面代码示例里我用Async/Await,如果你习惯同步写法,要留意上下文。这里不展开线程问题,避坑章会专门说。有了这些前置知识,你就能理解为什么很多示例代码里那些配置项看着像“玄学”了,其实每一项都有对应检查逻辑。接下来我按实际使用顺序,把连接、读点、订阅三个功能拆开讲。3. 把客户端拆开:连接、读点、订阅三步走3.1 建立连接:SelectEndpoint和Session.Create的配合连接这一步做的事情比表面上多得多:先要通过服务URL拿到服务端暴露的Endpoint列表,再根据安全策略选一个能用的,最后带上客户端配置去创建Session。代码写起来是三步,但每步都对应着一次网络握手。下面是最小可用的连接方法:Imports Opc.Ua Imports Opc.Ua.Client Private Async Function ConnectAsync(serverUrl As String) As Task(Of Session) ' 第一步:从服务端发现Endpoint,学习阶段先用不安全连接 Dim endpoint As EndpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity:=False, discoverTimeout:=10000) ' 第二步:创建客户端配置对象,应用名需要唯一 Dim config As New ApplicationConfiguration() config.ApplicationName = "VBOpcClient" config.ApplicationUri = "urn:vb:opc:client" config.SecurityConfiguration = New SecurityConfiguration() config.SecurityConfiguration.AutoAcceptUntrustedCertificates = True ' 第三步:创建Session,超时设为60秒,匿名UserIdentity Dim session As Session = Await Session.Create(config, endpoint, True, "SessionName", 60000, New UserIdentity(), Nothing) Return session End FunctionSelectEndpoint返回服务端的可用端点,useSecurity:=False的作用是跳过安全策略协商,先拿到地址。第二步的配置对象复用了之前的config,重点是ApplicationUri和AutoAccept。Session.Create看起来参数多,但前两个是配置和端点,第三个参数True表示需要创建一个Session实例,第四个是会话名称,最后两个分别是用户身份和订阅集合占位。如果你在UI线程调用,记得用Await而不是.Result,否则容易死锁。参数说明:serverUrl必须写完整,例如opc.tcp://192.168.1.10:4840或带路径的opc.tcp://192.168.1.10:4840/servicename。有些服务端省略路径也能连,但正规写法是加上服务名。discoverTimeout是毫秒,太短在大型服务器上会失败,我习惯设10秒。60000是sessionTimeout,指服务端认为会话有效的毫秒数,超过这个时间没有交互,服务端会回收会话,客户端需要重连。不建议一上来就用不安全连接,就算服务端允许匿名,你也至少把SecurityPolicy设成Basic256Sha256。学习阶段用不安全连接没问题,但接到生产服务器时要改回安全模式,否则数据明文传输,车间核心参数等于裸奔。3.2 读节点:NodeId的格式和DataValue的结构读一个节点的值,核心就两句话:把字符串转成NodeId,再通过Session读DataValue。代码里我尽量把逻辑抽成函数,方便复用:' 读取单个节点的示例,nodeIdStr形如:"ns=2;s=Machine/Temperature" Public Function ReadNodeValue(session As Session, nodeIdStr As String) As DataValue Dim nodeId As NodeId = NodeId.Parse(nodeIdStr) Return session.ReadValue(nodeId) End Function调用方式:Dim dv As DataValue = ReadNodeValue(session, "ns=2;s=Machine/Temperature") Console.WriteLine("Value={0}, Timestamp={1}", dv.Value, dv.SourceTimestamp)NodeId.Parse会自动识别ns=2;s=Tag1形式,其中ns是命名空间索引,s表示字符串标识。后面跟的是服务端地址空间里真正的标识符,而不是你在客户端自定义的名字。ReadValue返回DataValue,里面除了Value,还有SourceTimestamp和StatusCode。StatusCode等于Good才能用,否则读到的是错误码,不是实时值。参数说明:如果返回BadNodeIdUnknown,说明NodeId在服务端不存在,或者命名空间索引不对,这个坑在避坑章还要展开。读数组或者结构体也没问题,DataValue.Value会是数组或对象,你可以直接强转成对应的类型。如果节点是方法,要用CallMethod,不能读。3.3 订阅数据变化:MonitoredItem和Subscription的配合轮询在很多场景下能用,但变量一多就浪费带宽。OPC UA的订阅机制是把变量挂到一个Subscription上,服务端按设定的周期推送变化,模型比轮询优雅得多。代码也不复杂:' 订阅示例:把节点挂到订阅上,回调在数据变化时触发 Public Sub Subscribe(session As Session, nodeIdStr As String) Dim subscription As New Subscription() subscription.PublishingInterval = 500 ' 发布间隔,500ms subscription.LifetimeCount = 100 ' 生命周期计数,定时清理 subscription.MaxKeepAliveCount = 10 ' 心跳数量 Dim monitorItem As New MonitoredItem() monitorItem.StartNodeId = NodeId.Parse(nodeIdStr) monitorItem.SamplingInterval = -1 ' -1表示用服务端默认采样间隔 monitorItem.QueueSize = 1 AddHandler monitorItem.Notification, AddressOf OnDataChange subscription.AddItem(monitorItem) session.AddSubscription(subscription) subscription.Create() End Sub Private Sub OnDataChange(monitoredItem As MonitoredItem, args As MonitoredItemNotificationEventArgs) Dim value As DataValue = monitoredItem.LastValue Console.WriteLine("节点 {0} 新值: {1}", monitoredItem.StartNodeId, value.Value) End SubNew Subscription创建一个订阅,AddItem把监控项加进去,最后必须调用subscription.Create(),订阅才会被服务端接受,这一步漏了是最常见的坑。回调OnDataChange里取monitoredItem.LastValue,是订阅发生后服务端推送的最新值。参数说明:PublishingInterval是指服务端按这个时间间隔向客户端推送数据;SamplingInterval是指服务端对底层信号采样的间隔,设为-1时使用服务端默认值。这两者不是一回事,后者不能大于前者,否则客户端看到的数据会跳跃。QueueSize设为1只保留最新值,如果设成0则无限缓冲,回调会积压。LifetimeCount和MaxKeepAliveCount是订阅生命周期参数,和学习阶段关系不大,保持默认即可。在真实项目里,用订阅代替轮询能显著减少网络开销,尤其是变量多的时候。但要注意,如果服务端是那种只支持上报而不支持订阅的老版本,还是得退回到轮询。4. 排查与避坑:连接、读点、订阅中的四个翻车点4.1 连接失败:证书信任和URL格式是两大来源现象:程序抛BadSecurityModeRejected,或者连接超时,日志停留在“Discover servers”阶段。原因:80%是证书信任问题。服务端只信任它已知的客户端证书,而你本地还没有生成有效的应用证书,甚至证书压根没创建。剩下20%是URL格式错误,比如写成http://IP:Port,或者在端口后漏了服务名。OPC UA的标准地址是opc.tcp://开头,很多服务端在根目录也会有/UA/之类的路径,写错一个斜杠就找不到Endpoint。解决:学习阶段把AutoAcceptUntrustedCertificates设为True,能直接跳过证书校验;生产环境则要生成客户端证书,并把它手动添加到服务端的信任列表中。URL的话,不要自己猜,用服务端提供的说明或管理工具导出的地址。我通常在代码里输出endpoint.EndpointUrl看看实际选中的是什么,能省不少时间。4.2 读不到数据:NodeId的命名空间索引不对现象:连接成功,但读取时返回BadNodeIdUnknown,或者读到的数值明显不是你要的变量。原因:服务端的地址空间有多个命名空间,每个命名空间维护自己的节点集。ns=0是UA标准节点,业务变量一般在ns=2、ns=3甚至ns=8。你如果沿用别的项目的NodeId,或者自己在客户端里自定义了一个,自然找不到。另一个隐藏问题是标识符类型:有些变量用字符串标识,NodeId写法是ns=2;s=Tag1;有些用数字标识,写法是ns=2;i=1001;类型不对也会报节点不存在。解决:用浏览地址空间的方法拿到真实NodeId。我一般会在对接前先打印一次服务端所有节点的NodeId和路径,再把这些值写进配置文件。不要手敲长字符串,很容易眼花。4.3 订阅不触发:PublishingInterval和SamplingInterval的关系反了现象:订阅创建成功,也没有异常,但回调一次都不执行,或者触发频率和设置完全不搭。原因:这是典型的参数配合问题。订阅机制里,服务端按SamplingInterval采集信号,再按PublishingInterval打包发送给客户端。如果你把PublishingInterval设成10ms,而服务端采样间隔是500ms,客户端收到数据不会超过每秒2次。反过来,如果PublishingInterval设成5000ms,而采样间隔只有100ms,那100ms内多次变化会被合并成一次推送。还有一个常见失误:只调了AddSubscription而没有调subscription.Create(),服务端根本没收到订阅请求。解决:把两个间隔先设成一致(比如都1000ms),跑通了再按性能需求调。同时确认在创建完订阅的那行代码后面没有抛异常,最好打印订阅的SequenceNumber来验证。4.4 中文乱码:UA字符串编码与客户端默认编码不一致现象:变量读到的是中文,但控制台或日志显示成“烫烫烫”或者一堆问号。原因:OPC UA规范里字符串用UTF-8编码,而Windows下VB控制台的默认输出编码经常是GBK或ASCII。数据本身没有坏,是输出环节把它按错误的代码页解释了。如果写入时也乱码,那就更麻烦,说明你在写入前没有把字符串转成UTF-8字节。解决:在读取端设置控制台编码为UTF-8,或者读取后用Encoding.UTF8.GetString处理字节数组。写入端则用System.Text.Encoding.UTF8.GetBytes把字符串转成符合UA规范的字节流。不要用Convert.ToString这种侥幸做法,你看到的现象会变成“有时候对,有时候错”,非常磨人。5. 联调与改造:让客户端从“能跑”变成“能干”5.1 先搭一个可用的模拟服务端,把客户端跑通接入真实PLC之前,我强烈建议先在本地搭一个模拟服务端。常见做法是找一个带界面的UA模拟器,启动后它会自动建立一个地址空间,里面有一堆模拟变量,比如随机温度、转速、位移。你只需要把服务端地址填到客户端里,然后浏览地址空间,确认数据能读到,再在此基础上改业务逻辑。浏览地址空间的代码是排查NodeId的重要工具,我通常把它写成一个独立方法:' 浏览地址空间,打印前20个节点的NodeId和名称 Public Sub BrowseNodes(session As Session) Dim root As NodeId = ObjectIds.ObjectsFolder Dim nodesToBrowse As New BrowseDescriptionCollection() Dim nodeToBrowse As New BrowseDescription() nodeToBrowse.NodeId = root nodeToBrowse.Direction = BrowseDirection.Forward nodeToBrowse.NodeClassMask = UInt32.MaxValue nodeToBrowse.IncludeSubtypes = True nodeToBrowse.ResultMask = UInt32.MaxValue nodesToBrowse.Add(nodeToBrowse) Dim results As BrowseResultCollection = session.Browse(Nothing, nodesToBrowse, Nothing) For Each result As BrowseResult In results For Each ref As ReferenceDescription In result.References If ref.NodeId.IsNull() = False Then Console.WriteLine("{0} - {1}", ref.NodeId.ToString(), ref.DisplayName.Text) End If Next Next End Subroot从ObjectsFolder开始,Direction用Forward表示向下遍历子节点,NodeClassMask设为最大值表示不过滤节点类型,IncludeSubtypes保证把继承的节点也带出来。结果里每个ReferenceDescription的NodeId就是后续读写的目标。注意有一些系统变量会非常多,打印前20个即可,不然控制台刷屏。跑通之后,你会发现真正花时间的地方不是OPC UA本身,而是你的数据映射逻辑:从几十个NodeId里找出你要的那几个,然后安排合适的采集周期。模拟器正好能帮你把这个映射表定下来。5.2 从“能跑”到“好用”:超时、重连和日志三个改造点第一个改造点是超时。Session.Create里有个sessionTimeout参数,但那只决定服务端整理会话的周期,真正影响你用户体验的是每次读写的超时。你可以为每个读操作设置一个超时时间,也可以通过Session.Read的重载传入请求头。我一般会把超时统一配置到ApplicationConfiguration的RequestTimeout属性,避免每个方法都要传参数。第二个改造点是重连。PLC重启、网络闪断,OPC UA会话都会断掉。如果你不做处理,程序会一直卡在等待状态。常见做法是监听Session的SessionReconnectComplete事件,重连成功后之前建立的订阅不会自动修复,你需要重新执行订阅创建逻辑:' 监听会话重连完成事件 AddHandler session.SessionReconnectComplete, Sub(se As Session, e As EventArgs) RebuildSubscription() End Sub这里用AddHandler和Lambda表达式,VB语法里注意在事件回调中捕获session变量时,闭包会引用同一个会话对象,重连后session内部会切换通道,所以直接用se重新获取即可,不要缓存旧的Subscription对象。RebuildSubscription方法里做的就是3.3那几行,把它抽出来复用。第三个改造点是日志。Ua .NET Standard库本身的日志能告诉你每一层的握手状态,但默认不输出。你可以把SetTraceMask的组合设得细一点,比如只记录网络层和状态机变化,太多反而干扰。我自己会额外写一个简易日志类,把连接成功、订阅建立、每次数据变化的时间戳记录下来,排查时比翻库日志快得多。还有一个小技巧:把NodeId集合移到配置文件或者数据库里,不要在代码里硬编码。这样模拟器换服务端时,只改配置文件就能连上,不用重新编译。我经历过一次因为NodeId写死在代码里,结果现场变量名和服务端对不上,改了一晚上。从那以后,我每次对接前都会强制走一遍浏览地址空间的流程,把所有NodeId存成JSON。希望这篇笔记帮到你。本文还有配套的精品资源,点击获取
简介:这是面向工业自动化开发者的OPC UA客户端示例,基于Visual Basic(VB)编写,适合正在学习OPC UA通信协议,或需要在VB环境中快速实现数据采集与读写功能的工程师借鉴参考。压缩包大小15.26MB,共181个文件,以122个DLL类库为主,另含VB项目源码、XML配置、EXE可执行程序、PDB调试符号及少量工程缓存文件,其中TestOpcUaVb项目可直接打开调试。资源已有481人学习浏览。代码完整覆盖OPC UA客户端的核心交互流程,包括连接与安全认证、信息模型节点浏览、数据订阅通知、数值读写和异常处理等关键模块;配合配套的OPC Helper 2.02工具,可快速连接真实OPC服务器进行读写验证,理解UA客户端原理。通过研究项目源码,还能掌握VB环境下创建会话、构建订阅、订阅释放等SDK调用方式,为工业系统集成提供可复用的实践模板。1. 这份VB写的OPC UA客户端,能让你少走三个月弯路去年在车间调试一台老设备的联网采集,上位机是VB6改过来的,新设备却只提供OPC UA接口。网上搜了一圈,C#的示例倒是不少,VB的却少得可怜。翻遍几个工控群,最后找到这份用VB写的客户端工程,才把数据采回来。它不是完整的商业软件,只是一个能连接、能读点、能订阅的客户端骨架,代码风格直白,没有多余的封装,非常适合用来学习OPC UA客户端的开发思路。如果你也在做设备数据采集、MES对接、车间看板,又没有现成的C#同事帮你写底层,这份资源值得解压后对照着敲一遍。2. 先看懂OPC UA客户端:通信模型、核心对象和VB选型2.1 客户端面对的不再是DA的Item,而是Endpoint和NodeOPC UA的通信模型比传统OPC DA(那套COM/DCOM的冒号反斜杠)清晰得多。在客户端这边,你不再需要逐个“组态”变量,而是先和服务端建立一条安全通道,再通过节点ID直接操作数据。整个体系里,你只需要盯住几个核心对象:EndpointDescription描述的是服务端暴露的通信地址和安全策略;ApplicationConfiguration描述的是客户端自己的身份、证书和超时参数;Session是双方建立的一条逻辑会话,所有读写都发生在这条会话上;而Node是服务端地址空间里的数据节点,变量、参数、报警都抽象成Node,每个Node有一个唯一的NodeId。我一开始忽略了Node和旧的Item的区别,总想把变量当成独立的东西去配置,后来发现OPC UA更方便:地址空间本身就是一棵树,你想读什么,先通过NodeId定位那个节点,再在会话上操作它就行。选型上,VB环境要用UA库,最常见的选择是直接用官方发布的Ua .NET Standard库,它在VB.NET里可以直接引用,证书验证、会话管理、订阅回调都封装好了,适合把精力放在业务逻辑上。如果你的项目还在用VB6,那就只能走OPC DA兼容层或者另外写COM封装,代价大得多,我建议新项目直接上VB.NET加这个库。顺带说一个容易让人绕晕的点:OPC UA的地址空间是有层级结构的,根节点下面有ObjectsFolder,之后才是设备节点和数据变量。你用浏览工具看到的是一个树,而不是旧OPC DA里那种扁平的Group和Item列表。这个变化对代码的影响在于,你不用维护一个“项集合”,只要记住几个关键NodeId就能随时访问。真正费时间的反而是如何从服务端那棵大树上精准找到你要的变量,这个我会在联调章节里专门讲浏览代码。2.2 用VB引用UA .NET库:项目设置里的三个决定项先讲如何创建工程:新建一个VB项目,通过包管理器安装UaClient相关的包,然后在代码里引用命名空间。这里有三项设置会影响后面所有代码:第一,目标框架。我一般会选.NET Framework 4.7.2或更高,兼容性好,服务端和工具集都不容易出幺蛾子。选到.NET Core也可以,但要注意某些第三方模拟器只支持.NET Framework,会导致联调时来回折腾。第二,平台目标。如果你的项目默认是AnyCPU,在连接本地服务端时没问题,但一旦用到证书和某些本地库,偶尔会报BadImageFormatException。常见做法是把“平台目标”固定为x64,尤其是Windows 10以后的系统,对应服务端多为64位进程。第三,应用配置里ApplicationName和ApplicationUri。服务端在证书验证阶段会查看客户端证书的应用URI,如果证书和配置不一致,会直接拒绝连接。学习阶段可以先关闭证书验证,但你要知道这个坑存在。下面是一个最小配置的代码示例,放在Form的构造函数或Module启动段里就能用:' 创建客户端ApplicationConfiguration,注意证书路径可以暂时留空 Dim config As New ApplicationConfiguration() config.ApplicationName = "VB OPC UA Client" config.ApplicationUri = "urn:vb:opcuaclient" config.ApplicationType = ApplicationType.Client ' 学习阶段先自动接受未信任证书,生产环境必须改为False config.SecurityConfiguration = New SecurityConfiguration() config.SecurityConfiguration.AutoAcceptUntrustedCertificates = True ' 输出日志到控制台,方便排查连接问题 config.SetTraceMask(Utils.TraceMasks.StackTrace)这段配置里的ApplicationName要和你后面创建的证书匹配,否则服务端会回一个BadCertificateInvalid。ApplicationUri是全局唯一标识,建议用urn:开头的固定字符串,不要每次启动都随机生成,否则服务端会认为你在反复换身份。AutoAcceptUntrustedCertificates这个开关是学习时偷懒用的,放在车间现场必须关掉,不然后面证书问题会被人盯上。还有一点容易忽略:Ua .NET Standard库的大部分API是异步的,VB里要用Async/Await或者把返回的Task直接阻塞等待。我见过不少同事直接写Session.Create(...).Result,结果在UI线程上死锁。后面代码示例里我用Async/Await,如果你习惯同步写法,要留意上下文。这里不展开线程问题,避坑章会专门说。有了这些前置知识,你就能理解为什么很多示例代码里那些配置项看着像“玄学”了,其实每一项都有对应检查逻辑。接下来我按实际使用顺序,把连接、读点、订阅三个功能拆开讲。3. 把客户端拆开:连接、读点、订阅三步走3.1 建立连接:SelectEndpoint和Session.Create的配合连接这一步做的事情比表面上多得多:先要通过服务URL拿到服务端暴露的Endpoint列表,再根据安全策略选一个能用的,最后带上客户端配置去创建Session。代码写起来是三步,但每步都对应着一次网络握手。下面是最小可用的连接方法:Imports Opc.Ua Imports Opc.Ua.Client Private Async Function ConnectAsync(serverUrl As String) As Task(Of Session) ' 第一步:从服务端发现Endpoint,学习阶段先用不安全连接 Dim endpoint As EndpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity:=False, discoverTimeout:=10000) ' 第二步:创建客户端配置对象,应用名需要唯一 Dim config As New ApplicationConfiguration() config.ApplicationName = "VBOpcClient" config.ApplicationUri = "urn:vb:opc:client" config.SecurityConfiguration = New SecurityConfiguration() config.SecurityConfiguration.AutoAcceptUntrustedCertificates = True ' 第三步:创建Session,超时设为60秒,匿名UserIdentity Dim session As Session = Await Session.Create(config, endpoint, True, "SessionName", 60000, New UserIdentity(), Nothing) Return session End FunctionSelectEndpoint返回服务端的可用端点,useSecurity:=False的作用是跳过安全策略协商,先拿到地址。第二步的配置对象复用了之前的config,重点是ApplicationUri和AutoAccept。Session.Create看起来参数多,但前两个是配置和端点,第三个参数True表示需要创建一个Session实例,第四个是会话名称,最后两个分别是用户身份和订阅集合占位。如果你在UI线程调用,记得用Await而不是.Result,否则容易死锁。参数说明:serverUrl必须写完整,例如opc.tcp://192.168.1.10:4840或带路径的opc.tcp://192.168.1.10:4840/servicename。有些服务端省略路径也能连,但正规写法是加上服务名。discoverTimeout是毫秒,太短在大型服务器上会失败,我习惯设10秒。60000是sessionTimeout,指服务端认为会话有效的毫秒数,超过这个时间没有交互,服务端会回收会话,客户端需要重连。不建议一上来就用不安全连接,就算服务端允许匿名,你也至少把SecurityPolicy设成Basic256Sha256。学习阶段用不安全连接没问题,但接到生产服务器时要改回安全模式,否则数据明文传输,车间核心参数等于裸奔。3.2 读节点:NodeId的格式和DataValue的结构读一个节点的值,核心就两句话:把字符串转成NodeId,再通过Session读DataValue。代码里我尽量把逻辑抽成函数,方便复用:' 读取单个节点的示例,nodeIdStr形如:"ns=2;s=Machine/Temperature" Public Function ReadNodeValue(session As Session, nodeIdStr As String) As DataValue Dim nodeId As NodeId = NodeId.Parse(nodeIdStr) Return session.ReadValue(nodeId) End Function调用方式:Dim dv As DataValue = ReadNodeValue(session, "ns=2;s=Machine/Temperature") Console.WriteLine("Value={0}, Timestamp={1}", dv.Value, dv.SourceTimestamp)NodeId.Parse会自动识别ns=2;s=Tag1形式,其中ns是命名空间索引,s表示字符串标识。后面跟的是服务端地址空间里真正的标识符,而不是你在客户端自定义的名字。ReadValue返回DataValue,里面除了Value,还有SourceTimestamp和StatusCode。StatusCode等于Good才能用,否则读到的是错误码,不是实时值。参数说明:如果返回BadNodeIdUnknown,说明NodeId在服务端不存在,或者命名空间索引不对,这个坑在避坑章还要展开。读数组或者结构体也没问题,DataValue.Value会是数组或对象,你可以直接强转成对应的类型。如果节点是方法,要用CallMethod,不能读。3.3 订阅数据变化:MonitoredItem和Subscription的配合轮询在很多场景下能用,但变量一多就浪费带宽。OPC UA的订阅机制是把变量挂到一个Subscription上,服务端按设定的周期推送变化,模型比轮询优雅得多。代码也不复杂:' 订阅示例:把节点挂到订阅上,回调在数据变化时触发 Public Sub Subscribe(session As Session, nodeIdStr As String) Dim subscription As New Subscription() subscription.PublishingInterval = 500 ' 发布间隔,500ms subscription.LifetimeCount = 100 ' 生命周期计数,定时清理 subscription.MaxKeepAliveCount = 10 ' 心跳数量 Dim monitorItem As New MonitoredItem() monitorItem.StartNodeId = NodeId.Parse(nodeIdStr) monitorItem.SamplingInterval = -1 ' -1表示用服务端默认采样间隔 monitorItem.QueueSize = 1 AddHandler monitorItem.Notification, AddressOf OnDataChange subscription.AddItem(monitorItem) session.AddSubscription(subscription) subscription.Create() End Sub Private Sub OnDataChange(monitoredItem As MonitoredItem, args As MonitoredItemNotificationEventArgs) Dim value As DataValue = monitoredItem.LastValue Console.WriteLine("节点 {0} 新值: {1}", monitoredItem.StartNodeId, value.Value) End SubNew Subscription创建一个订阅,AddItem把监控项加进去,最后必须调用subscription.Create(),订阅才会被服务端接受,这一步漏了是最常见的坑。回调OnDataChange里取monitoredItem.LastValue,是订阅发生后服务端推送的最新值。参数说明:PublishingInterval是指服务端按这个时间间隔向客户端推送数据;SamplingInterval是指服务端对底层信号采样的间隔,设为-1时使用服务端默认值。这两者不是一回事,后者不能大于前者,否则客户端看到的数据会跳跃。QueueSize设为1只保留最新值,如果设成0则无限缓冲,回调会积压。LifetimeCount和MaxKeepAliveCount是订阅生命周期参数,和学习阶段关系不大,保持默认即可。在真实项目里,用订阅代替轮询能显著减少网络开销,尤其是变量多的时候。但要注意,如果服务端是那种只支持上报而不支持订阅的老版本,还是得退回到轮询。4. 排查与避坑:连接、读点、订阅中的四个翻车点4.1 连接失败:证书信任和URL格式是两大来源现象:程序抛BadSecurityModeRejected,或者连接超时,日志停留在“Discover servers”阶段。原因:80%是证书信任问题。服务端只信任它已知的客户端证书,而你本地还没有生成有效的应用证书,甚至证书压根没创建。剩下20%是URL格式错误,比如写成http://IP:Port,或者在端口后漏了服务名。OPC UA的标准地址是opc.tcp://开头,很多服务端在根目录也会有/UA/之类的路径,写错一个斜杠就找不到Endpoint。解决:学习阶段把AutoAcceptUntrustedCertificates设为True,能直接跳过证书校验;生产环境则要生成客户端证书,并把它手动添加到服务端的信任列表中。URL的话,不要自己猜,用服务端提供的说明或管理工具导出的地址。我通常在代码里输出endpoint.EndpointUrl看看实际选中的是什么,能省不少时间。4.2 读不到数据:NodeId的命名空间索引不对现象:连接成功,但读取时返回BadNodeIdUnknown,或者读到的数值明显不是你要的变量。原因:服务端的地址空间有多个命名空间,每个命名空间维护自己的节点集。ns=0是UA标准节点,业务变量一般在ns=2、ns=3甚至ns=8。你如果沿用别的项目的NodeId,或者自己在客户端里自定义了一个,自然找不到。另一个隐藏问题是标识符类型:有些变量用字符串标识,NodeId写法是ns=2;s=Tag1;有些用数字标识,写法是ns=2;i=1001;类型不对也会报节点不存在。解决:用浏览地址空间的方法拿到真实NodeId。我一般会在对接前先打印一次服务端所有节点的NodeId和路径,再把这些值写进配置文件。不要手敲长字符串,很容易眼花。4.3 订阅不触发:PublishingInterval和SamplingInterval的关系反了现象:订阅创建成功,也没有异常,但回调一次都不执行,或者触发频率和设置完全不搭。原因:这是典型的参数配合问题。订阅机制里,服务端按SamplingInterval采集信号,再按PublishingInterval打包发送给客户端。如果你把PublishingInterval设成10ms,而服务端采样间隔是500ms,客户端收到数据不会超过每秒2次。反过来,如果PublishingInterval设成5000ms,而采样间隔只有100ms,那100ms内多次变化会被合并成一次推送。还有一个常见失误:只调了AddSubscription而没有调subscription.Create(),服务端根本没收到订阅请求。解决:把两个间隔先设成一致(比如都1000ms),跑通了再按性能需求调。同时确认在创建完订阅的那行代码后面没有抛异常,最好打印订阅的SequenceNumber来验证。4.4 中文乱码:UA字符串编码与客户端默认编码不一致现象:变量读到的是中文,但控制台或日志显示成“烫烫烫”或者一堆问号。原因:OPC UA规范里字符串用UTF-8编码,而Windows下VB控制台的默认输出编码经常是GBK或ASCII。数据本身没有坏,是输出环节把它按错误的代码页解释了。如果写入时也乱码,那就更麻烦,说明你在写入前没有把字符串转成UTF-8字节。解决:在读取端设置控制台编码为UTF-8,或者读取后用Encoding.UTF8.GetString处理字节数组。写入端则用System.Text.Encoding.UTF8.GetBytes把字符串转成符合UA规范的字节流。不要用Convert.ToString这种侥幸做法,你看到的现象会变成“有时候对,有时候错”,非常磨人。5. 联调与改造:让客户端从“能跑”变成“能干”5.1 先搭一个可用的模拟服务端,把客户端跑通接入真实PLC之前,我强烈建议先在本地搭一个模拟服务端。常见做法是找一个带界面的UA模拟器,启动后它会自动建立一个地址空间,里面有一堆模拟变量,比如随机温度、转速、位移。你只需要把服务端地址填到客户端里,然后浏览地址空间,确认数据能读到,再在此基础上改业务逻辑。浏览地址空间的代码是排查NodeId的重要工具,我通常把它写成一个独立方法:' 浏览地址空间,打印前20个节点的NodeId和名称 Public Sub BrowseNodes(session As Session) Dim root As NodeId = ObjectIds.ObjectsFolder Dim nodesToBrowse As New BrowseDescriptionCollection() Dim nodeToBrowse As New BrowseDescription() nodeToBrowse.NodeId = root nodeToBrowse.Direction = BrowseDirection.Forward nodeToBrowse.NodeClassMask = UInt32.MaxValue nodeToBrowse.IncludeSubtypes = True nodeToBrowse.ResultMask = UInt32.MaxValue nodesToBrowse.Add(nodeToBrowse) Dim results As BrowseResultCollection = session.Browse(Nothing, nodesToBrowse, Nothing) For Each result As BrowseResult In results For Each ref As ReferenceDescription In result.References If ref.NodeId.IsNull() = False Then Console.WriteLine("{0} - {1}", ref.NodeId.ToString(), ref.DisplayName.Text) End If Next Next End Subroot从ObjectsFolder开始,Direction用Forward表示向下遍历子节点,NodeClassMask设为最大值表示不过滤节点类型,IncludeSubtypes保证把继承的节点也带出来。结果里每个ReferenceDescription的NodeId就是后续读写的目标。注意有一些系统变量会非常多,打印前20个即可,不然控制台刷屏。跑通之后,你会发现真正花时间的地方不是OPC UA本身,而是你的数据映射逻辑:从几十个NodeId里找出你要的那几个,然后安排合适的采集周期。模拟器正好能帮你把这个映射表定下来。5.2 从“能跑”到“好用”:超时、重连和日志三个改造点第一个改造点是超时。Session.Create里有个sessionTimeout参数,但那只决定服务端整理会话的周期,真正影响你用户体验的是每次读写的超时。你可以为每个读操作设置一个超时时间,也可以通过Session.Read的重载传入请求头。我一般会把超时统一配置到ApplicationConfiguration的RequestTimeout属性,避免每个方法都要传参数。第二个改造点是重连。PLC重启、网络闪断,OPC UA会话都会断掉。如果你不做处理,程序会一直卡在等待状态。常见做法是监听Session的SessionReconnectComplete事件,重连成功后之前建立的订阅不会自动修复,你需要重新执行订阅创建逻辑:' 监听会话重连完成事件 AddHandler session.SessionReconnectComplete, Sub(se As Session, e As EventArgs) RebuildSubscription() End Sub这里用AddHandler和Lambda表达式,VB语法里注意在事件回调中捕获session变量时,闭包会引用同一个会话对象,重连后session内部会切换通道,所以直接用se重新获取即可,不要缓存旧的Subscription对象。RebuildSubscription方法里做的就是3.3那几行,把它抽出来复用。第三个改造点是日志。Ua .NET Standard库本身的日志能告诉你每一层的握手状态,但默认不输出。你可以把SetTraceMask的组合设得细一点,比如只记录网络层和状态机变化,太多反而干扰。我自己会额外写一个简易日志类,把连接成功、订阅建立、每次数据变化的时间戳记录下来,排查时比翻库日志快得多。还有一个小技巧:把NodeId集合移到配置文件或者数据库里,不要在代码里硬编码。这样模拟器换服务端时,只改配置文件就能连上,不用重新编译。我经历过一次因为NodeId写死在代码里,结果现场变量名和服务端对不上,改了一晚上。从那以后,我每次对接前都会强制走一遍浏览地址空间的流程,把所有NodeId存成JSON。希望这篇笔记帮到你。本文还有配套的精品资源,点击获取