OPC DA 客户端 - Snet Docs

📘 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

组与点位管理(高级)

ReadAsyncSubscribeAsync 会自动处理组(订阅使用默认组 "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 服务。specifOpcDaNetApi.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 之前绑定 OnDataEventAsyncSubscribeAsync 创建的组本身已处于订阅状态。UpdateRate 控制轮询频率。

Q: 启动时提示服务名不存在。 A: SName 必须与 GetServersArray 返回的名称完全一致。先枚举查看实际服务名,并确认 API 版本匹配。

Q: 64 位机器上还能用 DA 吗? A: 可以——进程以 x86 运行即可。机器本身无需 32 位,只有进程必须是 32 位。

相关文档