Chapter 13 OpcUa 驱动专题
OpcUaClient 驱动的目标:把"读一个节点、写一个节点"这件事变成和其它驱动完全一样的测点。你不需要在业务代码里碰 Session、Subscription、DataValue——通道负责连接与会话,组合(TagCbnt)负责一次读多个节点。
用法三步,和别的驱动没有区别:
- 配通道:XML 里写一个
driver="OpcUaClient"的<Channel>,填端点与(可选的)安全项(见 13.1); - 配测点:
<Tag address="ns=4;s=...">,地址就是 NodeId(见 13.2)。 - 注册
builder.AddOpcUaClientSupport()(见 13.3);
本系列文档只讲"怎么用、为什么这样设计、容易踩什么"。地址表、字段默认值、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 的四项
| 字段 | 默认 | 说明 |
|---|---|---|
AutoAcceptUntrustedCertificates | true | 自动接受不受信任的服务端证书。默认宽松(无法防中间人) |
RejectSHA1SignedCertificates | false | 是否拒绝 SHA1 签名证书。默认宽松(SHA1 已不安全) |
MinimumCertificateKeySize | 1024 | 可接受的最小密钥长度(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 / Modbus | OpcUa | |
|---|---|---|
| 地址 | 协议地址(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 的状态字) |