Chapter 13 OpcUa 驱动专题

OpcUaClient 驱动的目标:把"读一个节点、写一个节点"这件事变成和其它驱动完全一样的测点。你不需要在业务代码里碰 Session、Subscription、DataValue——通道负责连接与会话,组合(TagCbnt)负责一次读多个节点。

用法三步,和别的驱动没有区别:

  1. 配通道:XML 里写一个 driver="OpcUaClient" 的 <Channel>,填端点与(可选的)安全项(见 13.1);
  2. 配测点:<Tag address="ns=4;s=...">,地址就是 NodeId(见 13.2)。
  3. 注册 builder.AddOpcUaClientSupport()(见 13.3);

配套包:StdUnit.Tags.OpcUaClient。

本系列文档只讲"怎么用、为什么这样设计、容易踩什么"。地址表、字段默认值、API 清单请查 AI 参考手册 的 §5.5;两者冲突时以手册为准。

13.1 通道配置

<Project xmlns:ua="tags:opcuaclient"
         xsi:noNamespaceSchemaLocation="Schemas/tagsproject.xsd"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Channel name="OpcUaClient-2" driver="OpcUaClient">
    <ua:ClientName>client-name-1</ua:ClientName>            <!-- 默认 StdUnitTagsOpcUaClient -->
    <ua:ServerOpt>
      <ua:DiscoveryUrl>opc.tcp://192.168.10.68:4840</ua:DiscoveryUrl>   <!-- 默认 localhost -->
      <ua:UsePassword>false</ua:UsePassword>                <!-- 默认 false -->
      <ua:UserName></ua:UserName>
      <ua:Password></ua:Password>
    </ua:ServerOpt>
    <ua:SecurityOpt>
      <ua:AutoAcceptUntrustedCertificates>true</ua:AutoAcceptUntrustedCertificates>   <!-- 默认 true -->
      <ua:RejectSHA1SignedCertificates>false</ua:RejectSHA1SignedCertificates>        <!-- 默认 false -->
      <ua:MinimumCertificateKeySize>1024</ua:MinimumCertificateKeySize>              <!-- 默认 1024 -->
      <ua:CertificateStoreRoot></ua:CertificateStoreRoot>                            <!-- 默认留空表示使用内置默认根 -->
    </ua:SecurityOpt>
  </Channel>
</Project>

ServerOpt / SecurityOpt 的每个字段都可以写成元素或属性;值写错(如 UsePassword="maybe")会在加载期抛 TagsProjectXmlException 并带 Channel(名) 这种上下文信息以开发者方便诊断。

SecurityOpt 的四项

字段默认说明
AutoAcceptUntrustedCertificatestrue自动接受不受信任的服务端证书。默认宽松(无法防中间人)
RejectSHA1SignedCertificatesfalse是否拒绝 SHA1 签名证书。默认宽松(SHA1 已不安全)
MinimumCertificateKeySize1024可接受的最小密钥长度(ushort)。现代要求通常不低于 2048
CertificateStoreRoot留空证书库根目录;留空则使用 CommonApplicationData/OPC Foundation/CertificateStores

只要有一项仍是宽松值,构造通道时会输出一条 Warning,把宽松项列出来并提示收紧:

通道(OpcUaClient-2)使用偏宽松的证书校验设置:AutoAcceptUntrustedCertificates=true;RejectSHA1SignedCertificates=false;MinimumCertificateKeySize=1024。这会降低对中间人攻击的防护,生产环境请在 <SecurityOpt> 中收紧(装入受信任证书、拒绝 SHA1、密钥长度不低于 2048)

证书库路径在 Linux 上要特别注意:默认根来自 CommonApplicationData,在 Linux 上等于 /usr/share——通常,无相关文件权限的用户写不进去,证书库创建会失败。此时用 CertificateStoreRoot 指向一个可写目录(与 OPC 官方示例一致的位置):

<ua:CertificateStoreRoot>/home/you/.local/share/OPC Foundation/CertificateStores</ua:CertificateStoreRoot>

四个证书库(MachineDefault、UA Certificate Authorities、UA Applications、RejectedCertificates)都是该根目录的子目录,由库用 Path.Combine 拼出来——你只需给根。取值按原样使用,不做占位符替换(%CommonApplicationData% 之类在这里不会被展开)。

13.2 测点怎么写

地址就是 NodeId 字符串,可以用在 Tag 上(直连测点),也可以放在 TagCbnt 里(组合,批量读写):

<TagGrp name="g4" isEntry="true" channel="OpcUaClient-2" scanInterval="10">
  <TagCbnt name="输入" access="RO">
    <Tag name="放行按钮闭合状态" address="ns=4;s=|var|CODESYS Control Win V3 x64.Application.PLC_PRG.var3"></Tag>
    <Tag name="手动" address="ns=4;s=|var|CODESYS Control Win V3 x64.Application.PLC_PRG.var3"></Tag>
  </TagCbnt>
  <TagCbnt name="输出" access="RW">
    <Tag name="绿灯" address="ns=4;s=|var|CODESYS Control Win V3 x64.Application.PLC_PRG.var4"></Tag>
    <Tag name="红灯" address="ns=4;s=|var|CODESYS Control Win V3 x64.Application.PLC_PRG.var5"></Tag>
  </TagCbnt>
</TagGrp>

几点值得知道:

  • 不用写 type(与 S7/Modbus 不同):节点的类型由服务端定,而库以 object 透传。要用的时候自己用 GetTagValue<T>() 做严格转换,或用 Note 记录语义。
  • TagCbnt 是有意义的:组合内的子测点在一次轮询里合并成一次批量读(多个 NodeId 一次 Read)或者一次批量写(多个 NodeId 一次 Write),省的是网络往返——这是所有驱动里都通用的性能杠杆。
  • 同一个 NodeId 可以被多个子测点引用(别名):读的时候都能拿到值;写的时候只写一次(最后一个脏值生效),不会因为"重复键"报错。
  • 读失败(Bad)时:缓存、时间戳、通知都不改动,异常原样向上抛。
  • 成功时"先写完整个缓存,再逐点通知":处理器里读同组其它测点不会看到半更新的缓存。

13.3 注册与运行

builder.AddOpcUaClientSupport();          // 等价于同时注册 Channel + TagCbntBuilder + DirectTagBuilder + XSD provider
builder.EnableXmlSchemaValidation();      // 可选:让 <ua:ServerOpt>/<ua:SecurityOpt> 有智能提示与加载期校验

如果想要更精细地手动组合,可以分别调用 AddOpcUaClientChannel() / AddOpcUaClientTagCbntBuilder() / AddOpcUaClientDirectTagBuilder();如果只想改"什么算失败"的判定,见 13.5 的自定义一节。

运行方式和其它驱动完全一致——它只是"一个通道":

await ctrl.StartPollAsync(dir, root: null, hook: (proj, sp, ct) =>
{
    // 需要时在这里补逻辑组件
    return Task.CompletedTask;
});

13.4 它和 S7/Modbus 不一样的地方

S7 / ModbusOpcUa
地址协议地址(DB10.16、40001)节点标识(NodeId)字符串,如 `ns=4;s=
值类型由 type 决定(例如 INT16 对应 short)节点自身的类型是服务端说了算,所以库把它当 object 透传
一次读多个合并成一次协议请求(省网络往返)组合内一次 Read 多个节点(同样省往返)
读失败PLC 报错误码就抛异常节点状态码为 Bad 时抛异常

最后一条是 1.0 里定下的最重要语义,下一节单独讲。

13.5 坏点 = 读取失败(1.0 语义)

OPC UA 的每个节点各自带状态码。库不解释质量,只区分两件事:

状态处理理由
Bad抛 InvalidOperationException这多半是通信/配置问题(BadNotConnected、BadNodeIdUnknown…),只有抛出去才会走"崩溃、断开全部通道、重连"的恢复路径
Good / Uncertain照原样采集Uncertain 是"服务器确实给了值、它自己不确定",是否可信是业务问题

异常长这样(消息里带通道、节点、状态码,用于定位是哪个通道的哪个节点):

通道(OpcUaClient-2)读取节点值失败:节点=ns=4;s=|var|...PLC_PRG.var3 状态码=0x80340000(BadNodeIdUnknown)

为什么不是"跳过坏点 + 记日志"? 那样做会掐断重连路径,并且让值被无限期冻结、而入口看起来还活着——日志不是下游能用的接口,下游只有"异常(重试/崩溃策略、RunnerCrashed)"和"值本身"两个信号。代价是:永久性坏点(例如 NodeId 写错)会让该入口持续"失败、断开、重连",退避从 500ms 涨到 30s 封顶——但这是显式失败,不是静默冻结。

也正因为如此,库不引入"可信度"概念:需要"(测点值, 可信度)"时,请另加一个测点(例如 PLC 的状态字),而不是指望驱动层给质量位。

自定义"什么算失败"

默认口径是"只有 Bad 算失败"。如果你的现场希望更严或更松,注册时传一个委托即可——无需改库、也不写进 XML(它是代码,XML 表达不了):

// 更严:把 Uncertain 也算失败(例如只信 Good)
builder.AddOpcUaClientSupport(checkIsFailed: (err, value) =>
    value is null || (err is not null && ServiceResult.IsBad(err)) || StatusCode.IsUncertain(value.StatusCode));

// 更松:任何状态都照原样采集(读取永不因质量失败;代价是 Bad 时 value 可能是 null,由你承担)
builder.AddOpcUaClientSupport(checkIsFailed: (_, _) => false);

签名是 Func<ServiceResult?, DataValue?, bool>,返回 true 表示"这次读取算失败"。细粒度的 AddOpcUaClientChannel(checkIsFailed: ...) 也可以单独用。

判定时记得直接问 IsBad,不要取反 IsGood:IsGood(Uncertain) 是 false,取反会把"不确定"误判成失败。

13.6 排错清单

现象先看这里
入口反复"崩溃—重连",退避涨到 30s是不是永久性坏点?看异常消息里的 节点= 与 状态码=(例如 BadNodeIdUnknown 就是 NodeId 写错了)
日志出现"使用偏宽松的证书校验设置"预期行为(默认值就宽松);生产环境按提示在 <SecurityOpt> 里收紧
Linux 上证书库报 "File does not exist" / 权限不足用 <ua:CertificateStoreRoot> 指向可写目录(见 13.1 的提示)
连接不上、DiscoveryUrl 明明是 opc.tcp://...确认写的是端点 URL(DiscoveryUrl 支持 opc.tcp:// 与裸主机名);服务端是否允许该客户端证书
值是 null,但读取没报错说明该节点状态是 Good 却确实没有数据(或你把 checkIsFailed 设成了永不失败);库不做质量判断
想要"值 + 可信度"别在驱动层找:另加一个测点(例如 PLC 的状态字)