📘 OPC DA 客户端(COM/DCOM)
命名空间: Snet.Opc.da.client | 类: OpcDaClientOperate | 基类: DaqAbstract<OpcDaClientOperate, OpcDaClientData.Basics> | 接口: IDaq | 平台: 必须 x86(32 位)
经典 OPC Data Access 客户端,基于 COM/DCOM 协议,用于连接通过 OPC DA 1.0 / 2.0 / 3.0 服务端暴露标签的遗留工业系统(如 Knight.OPC.Server.Demo、Kepware、Matrikon OPC)。支持服务枚举、组与点位管理、读写、轮询订阅。
与其余 OPC 客户端一样,OpcDaClientOperate 是一个标准 IDaq 驱动——用 OnAsync 连接、用 ReadAsync/WriteAsync 读写点位、用 SubscribeAsync 订阅数据变化。SubscribeAsync 会自动创建并订阅组,常规场景完全无需手动管理组,用法与 OPC UA 客户端 完全一致。
OPC DA COM 互操作要求32 位进程。请将应用程序的目标平台设置为 x86。其余 OPC 组件不区分进程位数。
快速开始
using Snet.Opc.da.client;
using Snet.Model.data;
using Snet.Model.@enum;
using System.Collections.Concurrent;
// 创建并连接——OnAsync 内部枚举服务,找到 "SName" 后连接
var da = await OpcDaClientOperate.InstanceAsync(new OpcDaClientData.Basics
{
SName = "Knight.OPC.Server.Demo",
Host = "localhost",
ApiVerType = ApiVerType.COM_DA_20,
UserName = "shunnet",
Password = "shunnet"
});
await da.OnAsync();
// 批量读取——AddressName 为 OPC DA 点位 id(标签路径)
var address = new Address(new List<AddressDetails>
{
new("Temperature", DataType.Float),
new("Pressure", DataType.Float)
});
var result = await da.ReadAsync(address);
if (result.Status)
{
// ResultData 是以点位名为键的字典
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
// 写入——字典键为点位 id
var writeResult = await da.WriteAsync(new ConcurrentDictionary<string, object>
{
["Channel1.Device1.Tag1"] = 100.5f
});
// 订阅——默认组 "Snet" 自动创建并订阅;先绑定事件,再订阅
da.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"事件数据: {kv.Key} = {kv.Value.ResultValue}");
}
};
await da.SubscribeAsync(new Address(new AddressDetails("Temperature", DataType.Float)));
await da.DisposeAsync();
安装
dotnet add package Snet.Opc
Snet 自身无需额外 COM 注册;目标 OPC DA 服务端与 DCOM 环境必须可从应用所在机器访问(见常见问题)。
配置(OpcDaClientData.Basics)
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
SN |
string |
Guid.NewGuid().ToUpperNString() |
唯一标识符 |
SName |
string? |
"Knight.OPC.Server.Demo" |
OPC DA 服务 ProgID / 名称(必须与枚举到的服务名一致) |
ApiVerType |
ApiVerType |
ApiVerType.COM_DA_20 |
COM API 版本:COM_DA_10(1.x)、COM_DA_20(2.x)、COM_DA_30(3.x) |
Host |
string? |
"localhost" |
服务端主机名或 IP |
UserName |
string? |
null |
DCOM 凭据(为空使用当前 Windows 身份) |
Password |
string? |
null |
DCOM 凭据 |
Domain |
string? |
null |
与凭据关联的 Windows 域 |
UpdateRate |
int |
100 |
订阅更新频率(毫秒) |
SubscribeSingleGroupMaxCount |
int |
0 |
订阅单组最大数;0 = 不分组,如 100 = 1000 个点位自动分成 10 组 |
TaskNumber |
int |
5 |
订阅通知处理任务数(并发) |
ApiVerType 枚举(OpcDaClientData.ApiVerType):COM_DA_10 — 1.x API;COM_DA_20 — 2.x API;COM_DA_30 — 3.x API。
API 参考
IDaq 生命周期(继承)
| 方法 | 描述 |
|---|---|
Task<OperateResult> OnAsync(CancellationToken token = default) |
用 GetServersArray(按 ApiVerType/Host/凭据)枚举服务,找到 SName 后连接并启动任务池。找不到或连接失败返回"连接失败" |
Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default) |
停止任务、清空组与订阅并断开连接 |
Task<OperateResult> ReadAsync(Address address, CancellationToken token = default) |
批量读取点位值。ResultData:以点位名为键的 ConcurrentDictionary<string, AddressValue> |
Task<OperateResult> WriteAsync(ConcurrentDictionary<string, (object value, EncodingType? encodingType)> values, CancellationToken token = default) |
批量写入点位值(键为点位名;支持虚拟地址) |
Task<OperateResult> SubscribeAsync(Address address, CancellationToken token = default) |
订阅点位:SubscribeSingleGroupMaxCount > 0 时拆分为多组,否则加入默认组(AddNode(address, "", true),组自动创建并订阅)。先绑定 OnDataEventAsync |
Task<OperateResult> UnSubscribeAsync(Address address, CancellationToken token = default) |
从组中移除指定点位 |
Task<OperateResult> GetStatusAsync(CancellationToken token = default) |
已连接时返回 true("已连接") |
Task<OperateResult> GetBaseObjectAsync(CancellationToken token = default) |
ResultData:底层 OpcDaNetApi.Da.Server |
组与点位管理(高级)
ReadAsync 与 SubscribeAsync 会自动处理组(订阅使用默认组 "Snet")。仅当需要显式控制组时才使用以下方法——例如多个命名组、预订阅组或精细化移除。
| 方法 | 描述 |
|---|---|
Task<OperateResult> AddGroupAsync(string GroupName, bool IsSubscribed = false, CancellationToken token = default) / AddGroup(...) |
创建 OPC 组(订阅状态)。IsSubscribed = true 时接入内部数据变化处理 |
Task<OperateResult> AddNodeAsync(Address address, string GroupName, bool IsSubscribed = false, CancellationToken token = default) / AddNode(...) |
向组添加点位。GroupName 为空时创建/使用默认组 "Snet"。重复点位自动过滤 |
Task<OperateResult> UpdateGroupSubscribedStateAsync(string GroupName, bool IsSubscribed, CancellationToken token = default) / UpdateGroupSubscribedState(...) |
切换组的订阅状态(挂接/摘除数据变化处理) |
Task<OperateResult> RemoveGroupAsync(string GroupName, CancellationToken token = default) / RemoveGroup(...) |
取消订阅、释放并移除组 |
Task<OperateResult> RemoveNodeAsync(string NodeName, string? GroupName = null, CancellationToken token = default) / RemoveNode(...) |
移除点位。GroupName = null 时移除所有组中同名点位;NodeName = null 且组名非空时移除整个组;两者都为空则失败 |
服务枚举
| 方法 | 描述 |
|---|---|
static OpcDaNetApi.Server[]? GetServersArray(OpcDaNetApi.Specification specif, NetworkCredential network, string? host = "localhost") |
枚举主机上可用的 OPC DA 服务。specif 为 OpcDaNetApi.Specification 值(COM_DA_10/COM_DA_20/COM_DA_30);network 携带 DCOM 凭据。OnAsync 内部即用它枚举,并连接 Name 等于 SName 的服务 |
代码示例
通过 IDaq 批量读写
// 混合数据类型批量读取
var mixed = new Address(new List<AddressDetails>
{
new("Tag1", "Channel1.Device1.Tag1", DataType.Float),
new("Tag2", "Channel1.Device1.Tag2", DataType.Int),
new("Alarm", "Channel1.Device1.Alarm1", DataType.Bool)
});
var read = await da.ReadAsync(mixed);
if (read.Status)
{
var data = read.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}
// 按点位分别写值
var write = await da.WriteAsync(new ConcurrentDictionary<string, object>
{
["Channel1.Device1.Tag1"] = 99.5f,
["Channel1.Device1.Tag2"] = 42
});
分组订阅
var da = await OpcDaClientOperate.InstanceAsync(new OpcDaClientData.Basics
{
SName = "Knight.OPC.Server.Demo",
Host = "localhost",
ApiVerType = ApiVerType.COM_DA_20,
SubscribeSingleGroupMaxCount = 100 // 每 100 个点位自动分组
});
await da.OnAsync();
da.OnDataEventAsync += async (sender, e) =>
{
if (e.Status)
{
var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
foreach (var kv in data)
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {kv.Key} = {kv.Value.ResultValue}");
}
};
// 1000 个点位 → 10 组
await da.SubscribeAsync(new Address(all1000AddressDetails));
手动组与点位管理
// 显式创建组并添加点位
await da.AddGroupAsync("Group1", IsSubscribed: true);
var addr = new Address(new AddressDetails("Channel1.Device1.Tag1", "Channel1.Device1.Tag1", DataType.Float));
await da.AddNodeAsync(addr, "Group1");
// 切换组订阅状态
await da.UpdateGroupSubscribedStateAsync("Group1", IsSubscribed: true);
// 移除点位 / 组
await da.RemoveNodeAsync("Channel1.Device1.Tag1"); // 所有组中同名点位
await da.RemoveNodeAsync("Channel1.Device1.Tag1", "Group1"); // 指定组中的点位
await da.RemoveNodeAsync(null, "Group1"); // 移除整个组
await da.RemoveGroupAsync("Group1"); // 同上,显式写法
枚举服务
using System.Net;
OpcDaNetApi.Server[]? servers = OpcDaClientOperate.GetServersArray(
OpcDaNetApi.Specification.COM_DA_20, // DA 规范版本
new NetworkCredential("username", "password", "domain"), // DCOM 凭据(可为空)
host: "localhost");
foreach (var s in servers)
Console.WriteLine(s.Name);
常见问题
Q: 客户端报 COM/免注册错误。
A: OPC DA 要求32 位进程(平台目标设为 x86),且目标 OPC DA 服务端已在机器上注册(或可通过 DCOM 访问)。先用 GetServersArray 确认能枚举到服务。
Q: 点位标识符格式是什么?
A: OPC DA 点位 id——通常是标签路径,如 Channel1.Device1.Tag1,由 DA 服务端定义。它同时用作 AddressName 和读写结果的键。
Q: 需要手动创建组吗?
A: 不需要。ReadAsync 直接读取;SubscribeAsync 会自动创建并订阅默认组 "Snet"(SubscribeSingleGroupMaxCount > 0 时自动拆分多组)。手动组方法仅用于显式控制场景。
Q: 远程主机 DCOM 失败。
A: 在两台机器上配置 DCOM 安全(用户权限、防火墙放行 DCOM 端口、HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\... 权限),并在 Basics 中提供 UserName/Password/Domain。
Q: 收不到订阅事件。
A: 在 SubscribeAsync 之前绑定 OnDataEventAsync;SubscribeAsync 创建的组本身已处于订阅状态。UpdateRate 控制轮询频率。
Q: 启动时提示服务名不存在。
A: SName 必须与 GetServersArray 返回的名称完全一致。先枚举查看实际服务名,并确认 API 版本匹配。
Q: 64 位机器上还能用 DA 吗? A: 可以——进程以 x86 运行即可。机器本身无需 32 位,只有进程必须是 32 位。
相关文档
- OPC DA 客户端(HTTP 转发) — 同一批 DA 服务端走 HTTP 访问,无需 COM/DCOM
- OPC 协议族 — 四个 OPC 组件
- DaqAbstract 基类 — 生命周期与同步包装
- IDaq 接口 · 地址模型
