OpcUaHelper.Team 2.1.2
OpcUaHelper.Team
基于 OPC Foundation 官方 .NET Standard 栈(2.0.0,MIT 许可)二次封装的 OPC UA 客户端通讯库。 v2.0 完成官方栈 2.0 的适配:全异步 API、线程安全的会话管理、自动重连与订阅恢复,同时支持 .NET Framework 4.8 与 .NET 8/9/10,以标准 NuGet 包形式发布。
特性
- 开箱即连:一次
ConnectAsync完成端点发现、安全协商、证书引导(首次运行自动生成自签名客户端证书) - 全异步 API:读写、浏览、订阅、历史、方法调用全部基于 async/await,可并发调用
- 自动重连:心跳监测 +
SessionReconnectHandler重连,重连后重新创建受管理的原生订阅,轮询订阅继续读取新会话 - 双模式订阅:原生推送(
Native)与客户端轮询(Polling,应对服务器扫描慢导致推送不及时的场景)可在同一客户端并存,按节点及时性要求分别选用 - 线程安全:重连期间会话对象的替换在内部保证一致,业务代码无感知
- 批量操作:批量读、批量写(逐项返回成功标志)、浏览自动分页
- 按需节点树:
CreateBrowseTree创建树,展开时LoadChildrenAsync只读取直接子节点,缓存已加载分支 - 全量扫描:
BrowseAllNodesAsync递归扫描整棵节点树(深度限制、进度回调、分支容错、自动去重) - 事件通知:连接状态机(Disconnected/Connecting/Connected/Reconnecting)+ 通讯状态事件
- 可选日志:构造函数注入
ILogger即可接入任意日志体系,默认关闭日志,同时官方栈的遥测也使用同一个日志器 - 双框架:单包多目标
net48+net8.0(net8.0 资产向上兼容 .NET 9/10)
安装
NuGet 包 ID 与显示名称为 OpcUaHelper.Team。程序集仍为 OpcUaHelper.dll,命名空间仍为 OpcUaHelper,现有 using 和 API 调用无需修改。从旧包迁移时,请将 NuGet 引用由 OpcUaHelper 替换为 OpcUaHelper.Team。
# 从你们的私有 NuGet 源安装
dotnet add package OpcUaHelper.Team --version 2.1.2 -s http://your-nuget-server/v3/index.json
或 PackageReference:
<PackageReference Include="OpcUaHelper.Team" Version="2.1.2" />
快速上手
using Opc.Ua;
using OpcUaHelper;
await using var client = new OpcUaClient(new OpcUaClientOptions
{
ServerUrl = "opc.tcp://192.168.1.88:4840",
ApplicationName = "MyApp", // 决定证书主题与 pki 目录名
});
client.ConnectionStateChanged += (_, e) => Console.WriteLine($"[状态] {e}");
client.StatusChanged += (_, e) => Console.WriteLine($"[通讯] {e}");
await client.ConnectAsync();
// 读:原始 DataValue / 强类型 / 批量
var dv = await client.ReadNodeAsync("ns=2;s=Device1.Tag1");
var value = await client.ReadNodeAsync<float>("ns=2;s=Device1.Tag1");
var values = await client.ReadNodesAsync<double>(new[] { "ns=2;s=T1", "ns=2;s=T2", "ns=2;s=T3" });
// 写:单点 / 批量(逐项返回 bool)
await client.WriteNodeAsync("ns=2;s=Device1.Tag1", 123.4f);
var ok = await client.WriteNodesAsync(new[] { "ns=2;s=T1", "ns=2;s=T2" }, new object[] { 1, 2 });
// 浏览
var nodes = await client.BrowseAsync("i=85"); // Objects 根
foreach (var node in nodes)
{
Console.WriteLine(node); // 形如 "Device1 [ns=2;i=5001] (Object)"
}
// 节点选择界面:只加载第一层,之后按展开加载子节点
var tree = client.CreateBrowseTree();
var firstLevel = await tree.LoadChildrenAsync();
foreach (var node in firstLevel)
Console.WriteLine($"{node.Info.DisplayName} [{node.Info.NodeId}]");
// 用户展开 node 时调用 await node.LoadChildrenAsync(),重复展开复用缓存。
// 订阅(一条语句完成创建 + 监视 + 下发)
await client.SubscribeAsync("heart", new[] { "ns=2;s=Device1.Tag1" }, n =>
Console.WriteLine($"{n.NodeId} = {n.Value.WrappedValue} [{n.Value.StatusCode}]"));
// 历史 / 方法 / 属性
var history = await client.ReadHistoryRawAsync<float>("ns=2;s=T1", DateTime.Now.AddHours(-1), DateTime.Now);
var outputs = await client.CallMethodAsync("ns=2;s=Device1", "ns=2;s=Device1.Method1", arg1, arg2);
var attrs = await client.ReadNodeAttributesAsync("ns=2;s=T1");
await client.DisconnectAsync();
完整可运行示例见 samples/OpcUaHelper.Samples(.NET 8) 与 samples/OpcUaHelper.NetFrameworkSample(.NET Framework 4.8)。
登录身份
// 匿名(默认)
new OpcUaClientOptions { ServerUrl = "...", Identity = OpcUaIdentity.Anonymous() }
// 用户名密码
new OpcUaClientOptions { ServerUrl = "...", Identity = OpcUaIdentity.UserName("admin", "123456") }
// 证书:2.0 的私钥由 CertificateManager.CertificateProvider 解析,标识只保存元数据。
// 应用证书配置在 ConnectAsync 内初始化,因此用 IdentityFactory 延迟构造身份。
new OpcUaClientOptions
{
ServerUrl = "opc.tcp://localhost:4840",
IdentityFactory = (configuration, ct) => OpcUaIdentity.CertificateAsync(
new Opc.Ua.CertificateIdentifier
{
StoreType = Opc.Ua.CertificateStoreType.Directory,
StorePath = @"C:\MyApp\pki\users",
Thumbprint = "用户证书指纹",
},
configuration.CertificateManager.CertificateProvider,
cancellationToken: ct),
}
// 加密私钥可传入 ICertificatePasswordProvider;外部提供者须保持可用直到客户端释放。
常用配置项(OpcUaClientOptions)
| 属性 | 默认值 | 说明 |
|---|---|---|
ServerUrl |
opc.tcp://localhost:4840 |
服务器地址 |
ApplicationName |
OpcUaHelper |
应用名(证书 CN、pki 目录名、会话名) |
UseSecurity |
true |
优先选择加密端点 |
AllowInsecureFallback |
true |
没有安全端点时允许回退 None;设为 false 要求安全连接 |
IdentityFactory |
null |
配置初始化后的异步身份工厂,设置后优先于 Identity |
AutoAcceptUntrustedCertificates |
true |
自动信任服务器自签名证书(受控工业网络便捷项;高安全场景请改 false 并手工管理信任列表) |
RejectSha1SignedCertificates |
false |
是否拒绝 SHA-1 证书(为兼容老 PLC 默认不拒绝) |
CertificateStoreRoot |
%LocalAppData%\<应用名>\pki |
DirectoryStore 根目录(own/issuer/trusted/rejected) |
SessionTimeoutMilliseconds |
60000 | 会话超时 |
KeepAliveIntervalMilliseconds |
5000 | 心跳间隔,0 禁用 |
ReconnectPeriodMilliseconds |
10000 | 断线重连间隔,0 禁用自动重连 |
MaxMessageSizeBytes |
4194304 | 单报文上限,大批量读写可调大 |
DefaultSubscriptionPublishingIntervalMilliseconds |
1000 | 原生订阅默认发布间隔(可被单次订阅参数覆盖) |
DefaultSubscriptionKeepAliveCount |
10 | 原生订阅默认心跳计数(× 发布间隔 = 心跳周期) |
DefaultSubscriptionLifetimeCount |
30 | 原生订阅默认生存期计数(× 发布间隔 = 订阅存活时长),须 ≥ 3 × KeepAlive |
DefaultSubscriptionMaxNotificationsPerPublish |
0 | 原生订阅默认单次发布最大通知数,0 不限 |
DefaultSubscriptionPriority |
0 | 原生订阅默认优先级 0-255 |
订阅模式:原生推送 vs 客户端轮询
订阅时通过 OpcUaSubscriptionOptions.Mode 指定策略,同一个客户端上两种模式可以并存:
// 原生订阅(默认):服务器推送,网络流量最小,适合对及时性要求不高的节点
await client.SubscribeAsync("slow", tags, OnChange);
// 轮询订阅:客户端以短间隔批量读取、变化即通知
// 适合小 PLC 等服务器端扫描周期长、原生推送不及时的信号
await client.SubscribeAsync("fast", criticalTags, OnChange, new OpcUaSubscriptionOptions
{
Mode = OpcUaSubscriptionMode.Polling,
PollingIntervalMilliseconds = 50, // 实际最小 10ms,应大于一次读取往返耗时
NotifyOnFirstScan = false, // 首扫是否立即通知当前值(默认仅变化时通知)
});
| 原生(Native) | 轮询(Polling) | |
|---|---|---|
| 及时性 | 受服务器采样/发布间隔限制 | 由客户端轮询间隔决定 |
| 网络流量 | 仅变更时报文 | 每间隔一次批量读(N 个节点一个报文) |
| 重连恢复 | 重新创建受管理的订阅与监视项 | 继续读取当前会话 |
| 首次通知 | 服务器默认推送初始值 | 默认不通知,NotifyOnFirstScan 可开启 |
| 变化判定 | 服务器端(含死区等过滤器) | 值或状态码变化(时间戳不参与比较) |
轮询的节奏是"读完再等待":实际周期 = 轮询间隔 + 一轮读取耗时;未连接时自动跳过本轮,连接恢复后继续。
订阅的自定义与增量构建
订阅时长参数支持两级配置:客户端级默认(实例化 OpcUaClient 时一次性设置,对所有原生订阅生效)与单次订阅覆盖(OpcUaSubscriptionOptions,未设置的项继续继承客户端默认):
// 客户端级:一次性设置,之后所有原生订阅自动使用
OpcUaClient = new OpcUaClient(new OpcUaClientOptions
{
ServerUrl = "opc.tcp://192.168.1.88:4840",
ApplicationName = "TeamAAS-VM",
DefaultSubscriptionPublishingIntervalMilliseconds = 1000,
DefaultSubscriptionKeepAliveCount = 10,
DefaultSubscriptionLifetimeCount = 300,
});
// 直接订阅——不传参数,时长自动继承上面的默认
await client.SubscribeAsync("main", tags, OnChange);
// 个别订阅覆盖:只改发布间隔,其余仍继承客户端默认
await client.SubscribeAsync("fast", tags, OnChange,
new OpcUaSubscriptionOptions { PublishingIntervalMilliseconds = 100 });
// 兜底回调:在一等属性之后执行,可覆盖任意设置(自定义过滤器等高级用法)
await client.SubscribeAsync("evt", tags, OnChange,
new OpcUaSubscriptionOptions { ConfigureSubscription = sub => sub.Priority = 1 });
// 增量构建
var sub = await client.CreateSubscriptionAsync("main");
sub.AddMonitoredItem("ns=2;s=T1", OnChange, samplingIntervalMilliseconds: 200);
sub.AddMonitoredItems(new[] { "ns=2;s=T2", "ns=2;s=T3" }, OnChange);
await sub.ApplyChangesAsync(); // 原生模式统一下发;轮询模式立即生效,无需调用
// ...
await client.RemoveSubscriptionAsync("main");
// 高级用法:拿到底层对象(仅原生订阅;轮询订阅没有底层 Subscription)
Subscription raw = ((IOpcUaNativeSubscription)sub).Subscription;
ISession session = client.Session;
OpcUaSubscriptionOptions 全部属性(时长类默认 null = 继承 OpcUaClientOptions.DefaultSubscription*):
| 属性 | 默认值 | 说明 |
|---|---|---|
Mode |
Native |
订阅模式(原生推送 / 客户端轮询) |
PublishingIntervalMilliseconds |
null(客户端默认 1000) |
发布间隔,仅原生模式 |
KeepAliveCount |
null(客户端默认 10) |
心跳计数,心跳周期 = 计数 × 发布间隔,仅原生模式 |
LifetimeCount |
null(客户端默认 30) |
生存期计数,订阅存活 = 计数 × 发布间隔,仅原生模式;显式值与继承值组合后须 ≥ 3 × KeepAliveCount(规范要求,不满足抛 ArgumentException) |
MaxNotificationsPerPublish |
null(客户端默认 0) |
单次发布最大通知数,0 不限 |
Priority |
null(客户端默认 0) |
订阅优先级 0-255,服务器拥塞时高优先级先发 |
PollingIntervalMilliseconds |
100 | 轮询间隔,仅轮询模式(实际最小 10ms) |
NotifyOnFirstScan |
false |
轮询首扫是否立即通知当前值 |
ConfigureSubscription |
null |
底层 Subscription 兜底自定义回调,最后执行可覆盖一切 |
服务器可能修订请求值,实际生效值可随时查询(重连重建后自动更新):
var native = (IOpcUaNativeSubscription)sub;
native.CurrentPublishingIntervalMilliseconds; // 服务器实际生效的发布间隔
native.CurrentKeepAliveCount; // 实际 KeepAlive 计数
native.CurrentLifetimeCount; // 实际生存期计数
client.SessionTimeoutMilliseconds; // 会话协商生效的超时(毫秒),未连接时为 null
断点调试与超时
Visual Studio 断点会冻结进程的全部线程,OPC UA 客户端随之停发一切请求(心跳、Publish、通道续期),三个服务器端计时器开始倒数:
| 机制 | 默认超时 | 控制方式 |
|---|---|---|
| 订阅生存期 | LifetimeCount × 发布间隔 = 30 秒 |
OpcUaSubscriptionOptions.LifetimeCount |
| 会话超时 | 60 秒 | OpcUaClientOptions.SessionTimeoutMilliseconds |
| 客户端发现断线 | 心跳间隔 5 秒 | OpcUaClientOptions.KeepAliveIntervalMilliseconds(只影响发现速度,不影响是否断开) |
默认配置下断点停留超过 30 秒订阅即被服务器删除,超过 60 秒会话被关闭;恢复运行后库会自动重连并重建订阅。调试期建议放大前两项,实例化客户端时一次性设置即可:
OpcUaClient = new OpcUaClient(new OpcUaClientOptions
{
ServerUrl = EndpointUrl,
ApplicationName = "TeamAAS-VM",
Identity = OpcUaIdentity.Anonymous(),
// 调试期放大(生产环境改回默认 60_000 / 30)
SessionTimeoutMilliseconds = Debugger.IsAttached ? 600_000 : 60_000, // 会话超时 10 分钟
DefaultSubscriptionLifetimeCount = Debugger.IsAttached ? 600 : 30, // 订阅存活 1000ms × 600 = 10 分钟
});
// 之后所有原生订阅自动继承,无需每次传参
await OpcUaClient.SubscribeAsync("main", tags, OnChange);
注意:服务器可能按自身上限修订请求值,可用 CurrentLifetimeCount / SessionTimeoutMilliseconds 确认实际生效值;大超时仅建议调试期使用,生产环境会长时间占用服务器会话与订阅资源。彻底方案是把 OPC 客户端放到独立进程,断点只打业务进程。
订阅管理与重复订阅防护
订阅在客户端内以唯一 key 登记,可通过以下 API 查询与管理:
client.SubscriptionCount; // 当前订阅数量(含原生与轮询)
client.GetSubscription("main"); // 按键获取订阅,不存在时返回 null
client.GetSubscriptions(); // 全部订阅快照(键 → 订阅)
client.IsNodeSubscribed("ns=2;s=T1"); // 该节点是否已被任一订阅监视(全局,含等价写法判定)
sub.GetMonitoredTags(); // 该订阅正在监视的节点标识快照
await client.RemoveSubscriptionAsync("main"); // 移除单个订阅
await client.RemoveAllSubscriptionsAsync(); // 移除全部订阅
重复订阅在三个层面受到防护:
- 订阅键:用已存在的
key再次SubscribeAsync/CreateSubscriptionAsync会抛出ArgumentException;并发注册同名键时恰好一个成功,败者自动清理(不覆盖、不泄漏服务器端订阅)。 - 订阅内节点:
AddMonitoredItem(s)按 NodeId 去重(i=2258与ns=0;i=2258视为同一节点),同一订阅内已监视的节点静默跳过,以首次添加时的配置与回调为准。 - 跨订阅节点:同一个客户端内,一个节点只允许被一个订阅监视——无论两个订阅是不同键的原生订阅,还是原生与轮询混用,向第二个订阅添加已被占用的节点都会抛出
ArgumentException(指出冲突节点与持有它的订阅键),且整批全部不添加;移除持有方订阅后节点即可被其他订阅接管。
注意:通过
IOpcUaNativeSubscription.Subscription底层逃生口直接向官方栈添加的监视项不经过上述去重。
重连行为
- 心跳发现服务中断 → 状态变为
Reconnecting(ReconnectPeriodMilliseconds为 0 时直接变为Disconnected) - 官方
SessionReconnectHandler周期性重连;恢复会话后,删除旧的受管理原生订阅并按原始定义重新创建(回调保持不变) - 完成恢复后 →
Connected;轮询订阅继续读取新会话。重建会触发新的首值通知,底层 Subscription/MonitoredItem 对象会变化 - 服务器长期下线时持续重试,直到连接恢复或调用
DisconnectAsync
提示:事件回调可能来自后台线程,WinForms/WPF 中需要
Invoke/Dispatcher切换到 UI 线程。
构建 / 测试 / 打包
dotnet build OpcUaHelper.sln # 编译全部(库 + 示例 + 测试)
dotnet test # 单元测试 + 集成测试(环境变量 OPCUA_TEST_SERVER 指定服务器,默认 opc.tcp://192.168.1.88:4840,不可达真正报告为跳过)
dotnet test --filter Category=Integration --environment OPCUA_TEST_REQUIRED=1 --environment OPCUA_TEST_SERVER=opc.tcp://192.168.1.88:4840
# 打包(推荐):产出 .nupkg + .snupkg 到 artifacts/packages/local-feed,可直接作为本地包源
powershell -File publish-nuget.ps1 # 可选参数:-Version 2.0.1 -Configuration Release
# 推送到私有 V3 源
dotnet nuget push artifacts\packages\local-feed\OpcUaHelper.Team.2.1.2.nupkg \
-s http://your-nuget-server/v3/index.json -k YOUR_API_KEY
包内已包含:128×128 图标(OPC UA 官方字标)、内嵌 README(私有源包页直接展示)、 XML 文档注释、snupkg 符号包、MIT 许可与仓库信息。
按需浏览节点树(2.0.2)
var root = client.CreateBrowseTree("i=85"); // 创建根,不发 Browse 请求
var firstLevel = await root.LoadChildrenAsync(cancellationToken);
// 用户展开其中一个节点:
var children = await firstLevel[0].LoadChildrenAsync(cancellationToken);
OpcBrowseNode.Info 保留节点标识、类别、父标识和深度,Parent / Children 提供层级结构。ChildrenLoaded 为 false 表示尚未成功浏览,为 true 且 Children 为空表示确认没有子节点。每次加载只取得直接子节点,自动处理该层的分页;同一节点并发加载复用成功结果,重复展开不再请求。失败、取消可重试。过滤重复引用、当前祖先引用及外部服务器引用,不提前探测每个子节点是否有下级。
树没有 UI 依赖,调用方在 await 后更新界面绑定。连接/重连后创建新树;刷新时也创建新树并取消旧树正在进行的加载。图标和条件编辑的数据类型可在用户选中变量后读取。
全量扫描节点树(独立用途)
BrowseAsync 只返回单层子节点;需要枚举服务器全部变量(做完整点位清单、批量建模)时用 BrowseAllNodesAsync:
var nodes = await client.BrowseAllNodesAsync(
rootTag: "i=85", // 扫描根(默认整个 Objects 地址空间)
maxDepth: 0, // 0 = 不限层
progress: p => { /* 每个节点浏览前及扫描完成时回调;UI 刷新应节流 */ });
// 结果特性:
// - 每个元素带 ParentNodeId / Depth,可直接重建整棵树
// - NodeId 自动去重,交叉引用不会死循环
// - 单个分支无权限/受限时仅告警跳过,不影响整体扫描
// - Variable 节点带 TypeDefinitionId,可区分数据变量与属性
大服务器可能有数万节点,扫描耗时与节点数成正比,节点选择界面建议使用上述按需节点树,全量扫描只用于明确需要完整点位清单的场景。
从 OpcUaHelper 1.0 升级至 2.0
- 官方 Client/Configuration/Core/Types/证书依赖均解析为 2.0.0;日志抽象升至 10.0.12。消费项目如直接引用官方栈或 Microsoft.Extensions.*,需同步检查版本,避免降级冲突。
- 泛型读写继续接受 CLR 数值、枚举、DateTime、Guid、数组和多维数组。byte[] 保持 ByteString 语义;UA Byte 数组可显式传 ArrayOf
。需要 SDK 新类型时也可以读取 DateTimeUtc、Uuid、ByteString 或 ArrayOf 。 - 原始 DataValue、NodeId、StatusCode 等 SDK 类型已变为值类型。订阅回调使用 n.Value.WrappedValue / n.Value.StatusCode,不能再写 n.Value?.Value。日期可用 n.Value.WrappedValue.GetDateTime().ToDateTime();优先采用 ReadNodeAsync
等封装接口。 - BrowseReferencesAsync 现在返回 IList
,替代已移除的 ReferenceDescriptionCollection;原始服务调用使用 ArrayOf 和 Variant。 - 原来的 OpcUaIdentity.Certificate(X509Certificate2) 改为 CertificateAsync(CertificateIdentifier, ICertificateProvider, ...),通过 IdentityFactory 在配置初始化后创建,见上方示例。
- 2.0 官方端点筛选不再自动降级。封装层用 AllowInsecureFallback 显式保留 1.x 回退行为并写入日志;设为 false 时无安全端点会报错。
- 历史读取会自动续页,numValuesPerNode 仍作为总返回上限;达到上限或中途失败时释放续页标识。原生订阅在重连后重新创建,底层逃生口对象须重新获取。
官方 2.0.0 包包含 net48 资产,本项目继续提供 net48 与 net8.0。但其 Microsoft.Extensions.Http.Resilience / Telemetry 等 10.10.0 传递依赖会发出 net48 未支持/未测试警告,未隐藏这些警告。net48 示例已在真实服务器验证连接、日期读取、订阅和断开;这不等于上游所有功能对 Framework 的支持承诺。
测试分为离线单元测试、回环地址上的真实 UA 2.0 服务器测试、现场服务器集成测试。本机服务器验证安全端点、证书用户登录、CLR 值与数组读写、方法调用、历史分页、重连恢复;现场测试验证连接、浏览、读值、订阅和客户端通道中断恢复。设置 OPCUA_TEST_REQUIRED=1 时,服务器连接失败会使测试失败,不会被当作通过。
从旧版(dathlin 2.x)迁移
| 2.x | 1.0 |
|---|---|
client.ConnectServer(url) |
await client.ConnectAsync(url) |
client.ReadNode<T>(tag) |
await client.ReadNodeAsync<T>(tag)(Bad 状态码现在抛异常) |
client.ReadNodeAsync<T>(tag)(假异步) |
同名方法现在是真异步 |
client.WriteNode(tag, v) |
await client.WriteNodeAsync(tag, v) |
client.WriteNodes(tags, values) → bool |
await client.WriteNodesAsync(tags, values) → bool[] 逐项结果 |
client.AddSubscription(key, tag, cb) |
await client.SubscribeAsync(key, tags, cb) |
client.RemoveSubscription(key) |
await client.RemoveSubscriptionAsync(key) |
client.BrowseNodeReference(tag) |
await client.BrowseReferencesAsync(tag) 或 BrowseAsync(轻量 DTO) |
client.ReadHistoryRawDataValues(...) |
await client.ReadHistoryRawAsync(...) |
client.OpcStatusChange 事件 |
StatusChanged + 新增 ConnectionStateChanged |
WinForms 浏览窗体 FormBrowseServer |
已移除(后续按需作为独立工具项目提供) |
主要破坏性变化:库不再包含任何 WinForms UI;API 全异步;OpcUaClient 构造函数要求传入 OpcUaClientOptions。
注:v1.0 是参考旧版使用习惯的全新实现,未复制旧版代码,因此不再受旧版 LGPL 约束。
目标框架
| 包资产 | 消费者 |
|---|---|
net48 |
.NET Framework 4.8+(SDK 风格与旧式 csproj 均可) |
net8.0 |
.NET 8 / 9 / 10 |
直接运行依赖: OPCFoundation.NetStandard.Opc.Ua.Client(2.0.0,传递引入 Configuration/Core/Types)+ Microsoft.Extensions.Logging.Abstractions。
许可
本项目采用 MIT 许可(见 LICENSE)。整个依赖链均为宽松许可,可放心在闭源商业项目中使用:
| 组件 | 许可 |
|---|---|
| OpcUaHelper(本库,全新实现) | MIT |
| OPCFoundation.NetStandard.Opc.Ua.* 2.0.0 | MIT(以 2.0.0 NuGet 包内 LICENSE.txt 为准) |
| Microsoft.Extensions.Logging.Abstractions | MIT |
| BitFaster.Caching(官方栈传递依赖) | MIT |
2.0.1 浏览修正
- 进度增加
CurrentNodeId、BrowsedNodeCount、PendingNodeCount;浏览叶节点没有发现新节点时也会回调,保留原进度构造函数。 - 连续 16 页没有引用但仍带续页标识时,抛出明确异常并释放续页资源;少量空页仍正常处理。
- 只跳过受限或已删除的子节点,根节点失败、超时、通讯中断及分页错误会上报;取消统一为
OperationCanceledException,不会把未完成的扫描返回为成功。 maxDepth: 0扫描 Objects 下全部层级,不是耗时上限。大量数组元素也需逐个 Browse,某些 PLC 会耗时数分钟。应用应让连接完成后在后台扫描、观察已浏览数和待浏览数,避免把扫描完成作为设备初始化的前置条件。此修正保留全量节点,不通过截断深度省略节点。
No packages depend on OpcUaHelper.Team.
2.1.2:订阅时长参数两级开放。OpcUaClientOptions 新增客户端级默认 DefaultSubscriptionPublishingIntervalMilliseconds / DefaultSubscriptionKeepAliveCount / DefaultSubscriptionLifetimeCount / DefaultSubscriptionMaxNotificationsPerPublish / DefaultSubscriptionPriority(实例化时一次性设置,构造即校验 3 倍规则);OpcUaSubscriptionOptions 时长属性改为可空(null = 继承客户端默认,显式值覆盖单项,显式与继承组合后校验),ConfigureSubscription 兜底回调仍最后执行可覆盖一切;IOpcUaNativeSubscription 新增 CurrentPublishingIntervalMilliseconds / CurrentKeepAliveCount / CurrentLifetimeCount 查询服务器实际修订生效值;OpcUaClient 新增 SessionTimeoutMilliseconds 查询会话协商超时。断点调试导致的断连可用上述参数放大订阅生存期与会话超时。2.1.0:订阅管理增强。新增 GetSubscription/GetSubscriptions/IsNodeSubscribed/GetMonitoredTags 查询 API;同名订阅键并发注册改为原子登记(TryAdd),败者自动清理不再覆盖或泄漏;同一客户端内节点全局去重——订阅内按 NodeId 静默去重(等价写法视为同一节点),跨订阅键、跨原生/轮询策略重复添加同一节点抛出 ArgumentException 且整批拒绝,移除持有方订阅后节点可被其他订阅接管。
.NET Framework 4.8
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- OPCFoundation.NetStandard.Opc.Ua.Client (>= 2.0.1)
.NET 8.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- OPCFoundation.NetStandard.Opc.Ua.Client (>= 2.0.1)