本文档基于自写的 OPC_Test_Pro 项目编写,讲解 OPC DA 通讯的原理、句柄关系及每个方法的实现细节。
本文只用于对该项目的技术分析。项目为自用项目,源码不对外开源。


目录

  1. 项目概述
  2. 核心类结构
  3. 句柄详解(重点)
  4. 方法详解
  5. 完整使用流程
  6. 常见问题

1. 项目概述

1.1 项目结构

OPC_Test_Pro/
├── OPC_Test_Pro/           # 主程序(WinForm界面)
│   ├── FrmOPCDA.cs        # 主窗体(界面交互)
│   └── FrmOPCItemSelect.cs # 变量选择窗体
├── Related_OPCLib/         # OPC通讯核心库
│   ├── OPCDALib.cs        # OPC DA核心类
│   ├── OPCDAGroup.cs      # OPC组定义
│   ├── OPCDAItem.cs       # OPC项定义
│   └── OPCDAAsyncEventArgs.cs # 异步事件参数
└── Related_DataConvertLib/ # 数据转换库
    └── OperateResult.cs   # 操作结果类

1.2 核心依赖

  • OPCAutomation.dll: OPC DA COM 组件的.NET互操作程序集

2. 核心类结构

2.1 OPCDALib - 核心通讯类

位置: Related_OPCLib/OPCDALib.cs

核心属性:

private OPCServer opcServer;              // OPC服务器对象
private List<Array> serverHandleList;     // 服务器句柄列表(重要!)
public List<OPCDAGroup> OPCGroupList;     // OPC组列表

核心事件:

public event EventHandler<OPCDAAsyncEventArgs> OnReadCompleted;  // 异步读取完成事件
public event EventHandler<OPCDAAsyncEventArgs> OnDataChanged;    // 数据订阅变化事件

2.2 OPCDAGroup - OPC组定义

位置: Related_OPCLib/OPCDAGroup.cs

public class OPCDAGroup
{
    public bool IsActive { get; set; }          // 组是否激活
    public int UpdateRate { get; set; }         // 更新速率(毫秒)
    public string GroupName { get; set; }       // 组名称
    public int GroupHandle { get; set; }        // 组客户端句柄(用户自定义)
    public OPCGroup OPCGroup { get; set; }      // OPC组对象(系统创建)
    public OPCDAItem[] OPCDAItems { get; set; } // 包含的变量数组
}

2.3 OPCDAItem - OPC项(变量)定义

位置: Related_OPCLib/OPCDAItem.cs

public class OPCDAItem
{
    public string ItemId { get; set; }      // 变量ID(如 "Channel1.Device1.Tag1")
    public int ClientHandle { get; set; }   // 客户端句柄(用户自定义,唯一标识)
    public object Value { get; set; }       // 变量值
    public int ServerHandle { get; set; }   // 服务器句柄(系统分配,用于读写操作)
}

3. 句柄详解(重点)

3.1 句柄关系图

┌─────────────────────────────────────────────────────────────┐
│                         OPC 客户端                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐       ┌──────────────────────────────┐   │
│  │ OPCDAGroup   │       │ OPCDAItem[]                  │   │
│  ├──────────────┤       ├──────────────────────────────┤   │
│  │ GroupHandle  │◄──────┤ ClientHandle (0, 1, 2, ...)  │   │
│  │ = 0          │       │ ItemId                       │   │
│  └──────┬───────┘       │ "Channel1.Device1.Tag1"      │   │
│         │               │ "Channel1.Device1.Tag2"      │   │
│         │               └──────────────┬───────────────┘   │
│         │                              │                    │
│         │         添加变量时            │                    │
│         │         系统分配             │                    │
│         ▼                              ▼                    │
│  ┌──────────────┐              ┌────────────────┐          │
│  │ OPCGroup     │              │ ServerHandle[] │          │
│  │ (COM对象)    │              │ [0] 无效值     │          │
│  │              │              │ [1] 12345      │          │
│  │              │              │ [2] 12346      │          │
│  └──────────────┘              │ [3] 12347      │          │
│                                 └────────────────┘          │
│                                                              │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ 读写操作时使用
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                         OPC 服务器                          │
└─────────────────────────────────────────────────────────────┘

3.2 三种句柄详解

3.2.1 GroupHandle(组客户端句柄)
  • 定义者: 用户(程序员)
  • 用途: 唯一标识一个 OPC 组
  • 值范围: 用户自定义,通常是 0, 1, 2…
  • 使用场景:
    • 查找对应的 OPC 组
    • 异步操作的事务ID

示例 (FrmOPCDA.cs:125):

OPCDAGroup group = new OPCDAGroup()
{
    GroupName = "Group1",
    IsActive = true,
    GroupHandle = 0,  // 用户自定义组ID
};
3.2.2 ClientHandle(项客户端句柄)
  • 定义者: 用户(程序员)
  • 用途: 唯一标识一个 OPC 变量(项)
  • 值范围: 用户自定义,通常是 0, 1, 2…(对应变量在数组中的索引)
  • 重要特性: 必须在同一个组内唯一
  • 使用场景:
    • 异步读取时匹配返回的数据
    • 写入时指定要写入的变量

示例 (FrmOPCDA.cs:130-136):

for (int i = 0; i < selList.Count; i++)
{
    group.OPCDAItems[i] = new OPCDAItem()
    {
        ItemId = selList[i],
        ClientHandle = i  // 0, 1, 2, ...
    };
}
3.2.3 ServerHandle(服务器句柄)
  • 定义者: OPC 服务器系统自动分配
  • 用途: OPC 服务器内部标识变量,用于实际的读写操作
  • 值范围: 系统分配,通常是任意整数(如 12345, 12346…)
  • 重要特性:
    • 添加变量后由系统返回
    • 数组从索引 1 开始(索引 0 无效)
    • 必须配合 ServerHandle 使用

示例 (OPCDALib.cs:204-205):

opcItems.AddItems(count, itemIds, clentHandles,
                  out Array serverHandles,  // 系统返回的服务器句柄数组
                  out Array errors);

3.3 句柄转换关系

用户操作流程:
1. 定义 ClientHandle (0, 1, 2)
2. 添加变量到服务器
3. 服务器返回 ServerHandle [无用, 12345, 12346, 12347]
4. 读写时通过 ClientHandle 找到对应的 ServerHandle

读取流程:
ClientHandle(1) ──查找──> ServerHandle数组索引2 ──获取──> ServerHandle值(12346) ──调用──> SyncRead()

代码示例 (OPCDALib.cs:362):

// 通过 ClientHandle 找到在数组中的位置
int itemIndex = this.OPCGroupList[index].OPCDAItems.ToList()
    .FindIndex(c => c.ClientHandle == clientHandle);

// +1 是因为 ServerHandle 数组从索引 1 开始
serverHandle.SetValue(
    this.serverHandleList[index].GetValue(itemIndex + 1),
    1
);

4. 方法详解

4.1 连接相关方法

4.1.1 GetOPCServerNodes - 获取服务器节点列表

位置: OPCDALib.cs:24-48

原理:

1. 获取本机IP地址列表
2. 通过DNS反向查询获取主机名
3. 过滤并去重,返回可用的OPC服务器节点

返回示例:

["DESKTOP-ABC123", "192.168.1.100"]
4.1.2 GetOPCServerNames - 获取OPC服务器名称列表

位置: OPCDALib.cs:54-73

原理:

1. 创建 OPCServer 对象
2. 调用 GetOPCServers() 获取本机所有OPC服务器
3. 返回服务器名称列表

返回示例:

["Kepware.KEPServerEX.V6", "OPC.SimaticNET"]
4.1.3 Connect - 建立OPC连接

位置: OPCDALib.cs:81-98

参数:

  • serverNode: 服务器节点(IP或主机名)
  • serverName: OPC服务器名称

原理:

1. 创建新的 OPCServer 实例
2. 调用 Connect(serverName, serverNode) 连接到OPC服务器
3. 成功返回 OperateResult.CreateSuccessResult()
4. 失败捕获异常并返回错误信息

调用示例:

var result = objOPC.Connect("DESKTOP-ABC123", "Kepware.KEPServerEX.V6");

4.2 变量管理方法

4.2.1 GetOPCBrower - 获取OPC变量列表

位置: OPCDALib.cs:115-131

原理:

1. 创建 OPCBrowser 对象
2. 调用 ShowBranches() 显示分支
3. 调用 ShowLeafs(true) 显示叶子节点
4. 遍历返回所有变量路径

返回示例:

[
    "Channel1.Device1.Tag1",
    "Channel1.Device1.Tag2",
    "Channel2.Device1.Tag1"
]
4.2.2 InitOPCGroup - 初始化OPC组(核心方法)

位置: OPCDALib.cs:137-164

参数:

  • opcGroupList: OPC组列表

原理详解:

1. 保存 OPCGroupList 到类属性(供后续读写使用)
2. 清空所有旧的OPC组
3. 设置默认属性:
   - DefaultGroupDeadband = 0 (死区为0,任何变化都触发)
   - DefaultGroupIsActive = true (组默认激活)
4. 遍历每个组,调用 AddOPCGroup() 添加
5. 返回操作结果

重要代码 (OPCDALib.cs:152):

serverHandleList = new List<Array>(); // 清空并重新初始化
4.2.3 AddOPCGroup - 添加单个OPC组(私有方法)

位置: OPCDALib.cs:171-214

原理详解:

1. 创建 OPCGroup 对象
   ├─ 设置 IsActive = true
   ├─ 设置 DeadBand = 0
   ├─ 设置 IsSubscribed = true (启用订阅)
   ├─ 设置 UpdateRate
   └─ 设置 ClientHandle = GroupHandle

2. 绑定事件:
   ├─ AsyncReadComplete (异步读取完成)
   ├─ DataChange (数据变化订阅)
   └─ AsyncWriteComplete (异步写入完成)

3. 添加OPC项(变量):
   ├─ 创建数组(长度 = 变量数 + 1)
   │   ├─ itemIds: 存储变量ID
   │   └─ clientHandles: 存储客户端句柄
   │
   ├─ 填充数组(从索引1开始,索引0无效):
   │   ├─ itemIds.SetValue(ItemId, i + 1)
   │   └─ clientHandles.SetValue(ClientHandle, i + 1)
   │
   ├─ 调用 AddItems() 添加到服务器
   │   └─ 返回 serverHandles (服务器句柄数组)
   │
   └─ 保存 serverHandles 到 serverHandleList

关键代码 (OPCDALib.cs:193-205):

Array itemIds = Array.CreateInstance(typeof(string), count + 1);
Array clentHandles = Array.CreateInstance(typeof(int), count + 1);

// OPC 要求从索引 1 开始
for (int i = 0; i < count; i++)
{
    itemIds.SetValue(opcGroup.OPCDAItems[i].ItemId, i + 1);
    clentHandles.SetValue(opcGroup.OPCDAItems[i].ClientHandle, i + 1);
}

opcItems.AddItems(count, itemIds, clentHandles,
                  out Array serverHandles,  // 服务器返回
                  out Array errors);

serverHandleList.Add(serverHandles);  // 保存供后续使用

4.3 读取方法

4.3.1 SyncRead - 同步读取

位置: OPCDALib.cs:273-299

原理:

遍历每个 OPC 组:
    1. 获取该组的 ServerHandle 数组
    2. 调用 SyncRead() 同步读取
    3. 参数说明:
       - Source: 1 (从缓存读取)
       - ItemCount: 变量数量
       - ServerHandles: 服务器句柄数组
    4. 返回读取到的值数组
    5. 转换为 object[] 并添加到结果列表

调用示例 (FrmOPCDA.cs:178):

List<object[]> value = this.objOPC.SyncRead();
// value[0] 包含第一个组所有变量的值
4.3.2 AsyncRead - 异步读取

位置: OPCDALib.cs:327-345

原理:

遍历每个 OPC 组:
    1. 获取该组的 ServerHandle 数组
    2. 调用 AsyncRead() 发起异步读取请求
    3. 参数说明:
       - ItemCount: 变量数量
       - ServerHandles: 服务器句柄数组
       - Errors: 返回错误码数组
       - TransactionID: 设为 GroupHandle(用于回调时识别)
       - CancelID: 返回的取消ID
    4. 读取完成后触发 AsyncReadComplete 事件

事件处理 (FrmOPCDA.cs:213-239):

private void ObjOPC_OnReadCompleted(object sender, OPCDAAsyncEventArgs e)
{
    // 1. 通过 GroupHandle 找到对应的组
    int groupIndex = this.objOPC.OPCGroupList
        .FindIndex(c => c.GroupHandle == e.GroupHandle);

    // 2. 通过 ClientHandle 匹配,将值赋给对应的 Item
    for (int i = 0; i < opcDAItems.Length; i++)
    {
        for (int j = 0; j < e.Count; j++)
        {
            if (opcDAItems[i].ClientHandle == e.ClientItemsHandle[j])
            {
                opcDAItems[i].Value = e.Value[j];
            }
        }
    }
}

4.4 订阅方法

4.4.1 开启订阅

位置: FrmOPCDA.cs:246-256

原理:

1. 订阅 OnDataChanged 事件
2. 服务器自动检测数据变化
3. 变化时触发 DataChange 事件
4. 无需主动调用读取方法

事件处理 (FrmOPCDA.cs:258-281):

private void ObjOPC_OnDataChanged(object sender, OPCDAAsyncEventArgs e)
{
    // e.ClientItemsHandle: 变化的变量 ClientHandle 数组
    // e.Value: 变化后的值数组

    for (int i = 0; i < e.Count; i++)
    {
        int handle = e.ClientItemsHandle[i];

        // 遍历所有组和变量,找到匹配的 ClientHandle
        for (int j = 0; j < this.objOPC.OPCGroupList.Count; j++)
        {
            for (int k = 0; k < this.objOPC.OPCGroupList[j].OPCDAItems.Length; k++)
            {
                if (this.objOPC.OPCGroupList[j].OPCDAItems[k].ClientHandle == handle)
                {
                    // 更新值
                    this.objOPC.OPCGroupList[j].OPCDAItems[k].Value = e.Value[i];
                }
            }
        }
    }
}

4.5 写入方法

4.5.1 SyncWrite - 同步写入

位置: OPCDALib.cs:353-380

参数:

  • value: 要写入的值
  • groupHandle: 组句柄
  • clientHandle: 项句柄

原理详解:

1. 通过 groupHandle 找到对应的 OPC 组索引
2. 通过 clientHandle 找到对应的 OPC 项索引
3. 从 serverHandleList 获取 ServerHandle:
   ├─ itemIndex = FindIndex(ClientHandle == clientHandle)
   ├─ serverHandleValue = serverHandleList[itemIndex + 1](注意: +1 是因为数组从索引1开始)
4. 创建写入数组(长度为2,使用索引1:
   ├─ writeServerHandles.SetValue(serverHandleValue, 1)
   └─ writeValues.SetValue(value, 1)
5. 调用 SyncWrite(1, writeServerHandles, writeValues, out errors)

关键代码 (OPCDALib.cs:362):

// 找到 ClientHandle 对应的索引位置
int itemIndex = this.OPCGroupList[index].OPCDAItems.ToList()
    .FindIndex(c => c.ClientHandle == clientHandle);

// 从 serverHandleList 获取对应的 ServerHandle(+1 是关键)
serverHandle.SetValue(
    this.serverHandleList[index].GetValue(itemIndex + 1),
    1
);

调用示例 (FrmOPCDA.cs:312):

// 正确示例: 使用变量下拉框的 SelectedIndex
var result = this.objOPC.SyncWrite(
    this.txt_SetValue.Text.Trim(),  // 写入的值
    0,                               // GroupHandle
    this.cmb_SetVariable.SelectedIndex // ClientHandle (0, 1, 2...)
);

❌ 错误示例:

// 错误: 使用了服务器名称下拉框的索引
var result = this.objOPC.SyncWrite(
    "100",
    0,
    this.cmb_ServerName.SelectedIndex  // 这是服务器索引,不是变量句柄!
);
// 结果: 找不到对应的 ClientHandle,导致索引超限错误
4.5.2 AsyncWrite - 异步写入

位置: OPCDALib.cs:389-416

参数: 与 SyncWrite 相同

原理:

1. 参数准备与 SyncWrite 完全相同
2. 调用 AsyncWrite() 而非 SyncWrite():
   AsyncWrite(1, serverHandle, values, out errors, transactionID, out cancelID)
3. 写入完成后触发 AsyncWriteComplete 事件

差异对比:

特性SyncWriteAsyncWrite
阻塞等待✅ 是❌ 否
返回时机写入完成后返回立即返回
适用场景单次少量写入批量写入
事件回调AsyncWriteComplete

5. 完整使用流程

5.1 初始化流程

// 步骤1: 创建 OPC 对象
OPCDALib objOPC = new OPCDALib();

// 步骤2: 获取服务器节点
var nodes = objOPC.GetOPCServerNodes();
// 返回: ["DESKTOP-ABC123"]

// 步骤3: 获取OPC服务器名称
var serverNames = objOPC.GetOPCServerNames("DESKTOP-ABC123XXX");
// 返回: ["Kepware.KEPServerEX.V6"]

// 步骤4: 建立连接
var result = objOPC.Connect("DESKTOP-ABC123XXX", "Kepware.KEPServerEX.V6");
if (!result.IsSuccess)
{
    MessageBox.Show("连接失败: " + result.Message);
    return;
}

// 步骤5: 获取变量列表
var varList = objOPC.GetOPCBrower();
// 返回: ["Channel1.Device1.Tag1", "Channel1.Device1.Tag2", ...]

// 步骤6: 选择变量并创建OPC组
List<string> selectedVars = new List<string>()
{
    "Channel1.Device1.Tag1",
    "Channel1.Device1.Tag2"
};

OPCDAGroup group = new OPCDAGroup()
{
    GroupName = "Group1",
    IsActive = true,
    GroupHandle = 0,
    UpdateRate = 1000
};

group.OPCDAItems = new OPCDAItem[selectedVars.Count];
for (int i = 0; i < selectedVars.Count; i++)
{
    group.OPCDAItems[i] = new OPCDAItem()
    {
        ItemId = selectedVars[i],
        ClientHandle = i  // 0, 1
    };
}

// 步骤7: 初始化OPC组
List<OPCDAGroup> groups = new List<OPCDAGroup>() { group };
result = objOPC.InitOPCGroup(groups);
if (!result.IsSuccess)
{
    MessageBox.Show("初始化失败: " + result.Message);
    return;
}

5.2 读取流程

// 方式1: 同步读取
List<object[]> values = objOPC.SyncRead();
if (values != null && values.Count > 0)
{
    foreach (var val in values[0])
    {
        Console.Write(val.ToString() + " ");
    }
}

// 方式2: 异步读取
// 订阅事件
objOPC.OnReadCompleted += (sender, e) =>
{
    int groupIndex = objOPC.OPCGroupList.FindIndex(g => g.GroupHandle == e.GroupHandle);

    for (int i = 0; i < objOPC.OPCGroupList[groupIndex].OPCDAItems.Length; i++)
    {
        for (int j = 0; j < e.Count; j++)
        {
            if (objOPC.OPCGroupList[groupIndex].OPCDAItems[i].ClientHandle == e.ClientItemsHandle[j])
            {
                var item = objOPC.OPCGroupList[groupIndex].OPCDAItems[i];
                Console.WriteLine($"{item.ItemId} = {e.Value[j]}");
            }
        }
    }
};

// 发起异步读取
objOPC.AsyncRead();

5.3 订阅流程

// 开启订阅
objOPC.OnDataChanged += (sender, e) =>
{
    for (int i = 0; i < e.Count; i++)
    {
        int handle = e.ClientItemsHandle[i];

        foreach (var group in objOPC.OPCGroupList)
        {
            foreach (var item in group.OPCDAItems)
            {
                if (item.ClientHandle == handle)
                {
                    Console.WriteLine($"{item.ItemId} 变化: {e.Value[i]}");
                }
            }
        }
    }
};

// 注意: IsSubscribed = true 时自动订阅,无需额外操作

5.4 写入流程

// 同步写入
var result = objOPC.SyncWrite(
    "100",        // 要写入的值
    0,            // GroupHandle
    0             // ClientHandle (第一个变量)
);

if (result.IsSuccess)
{
    Console.WriteLine("写入成功");
}
else
{
    Console.WriteLine("写入失败: " + result.Message);
}

// 异步写入
result = objOPC.AsyncWrite(
    "200",
    0,
    1  // 写入第二个变量
);

5.5 断开连接

objOPC.DisConnect();

6. 常见问题

6.1 索引超限错误

问题: 调用 SyncWrite()AsyncWrite() 时报"索引超限"错误

原因:

  • 传入了错误的 clientHandle
  • 通常混淆了下拉框的 SelectedIndex

解决方案:

// ❌ 错误
var result = objOPC.SyncWrite(value, 0, cmb_ServerName.SelectedIndex);

// ✅ 正确
var result = objOPC.SyncWrite(value, 0, cmb_SetVariable.SelectedIndex);

6.2 为什么数组从索引1开始?

原因: OPC DA COM 规范要求从索引1开始,索引0保留未使用

代码处理:

// 创建数组时长度 = 变量数 + 1
Array itemIds = Array.CreateInstance(typeof(string), count + 1);

// 填充时从索引1开始
for (int i = 0; i < count; i++)
{
    itemIds.SetValue(opcGroup.OPCDAItems[i].ItemId, i + 1);
}

// 访问时也要 +1
object serverHandle = serverHandles.GetValue(itemIndex + 1);

6.3 ClientHandle vs ServerHandle 何时使用?

场景使用句柄原因
异步读取回调ClientHandle服务器返回的是 ClientHandle
数据订阅回调ClientHandle服务器返回的是 ClientHandle
同步读取ServerHandle需要传递给 OPC 服务器
同步写入ServerHandle需要传递给 OPC 服务器
异步写入ServerHandle需要传递给 OPC 服务器

6.4 如何添加多个OPC组?

OPCDAGroup group1 = new OPCDAGroup()
{
    GroupName = "Group1",
    GroupHandle = 0,
    OPCDAItems = new OPCDAItem[] { /* ... */ }
};

OPCDAGroup group2 = new OPCDAGroup()
{
    GroupName = "Group2",
    GroupHandle = 1,  // 不同的 GroupHandle
    OPCDAItems = new OPCDAItem[] { /* ... */ }
};

List<OPCDAGroup> groups = new List<OPCDAGroup>() { group1, group2 };
objOPC.InitOPCGroup(groups);

6.5 异步写入完成后如何处理?

当前代码: AsyncWriteComplete 事件为空

建议添加:

private void KepGroup_AsyncWriteComplete(int TransactionID, int NumItems,
                                         ref Array ClientHandles, ref Array Errors)
{
    if (OnWriteCompleted != null)
    {
        var e = new OPCDAAsyncEventArgs()
        {
            GroupHandle = TransactionID,
            Count = NumItems,
            ClientItemsHandle = ArrayToIntArray(ClientHandles),
            Errors = ArrayToIntArray(Errors)
        };

        OnWriteCompleted(this, e);
    }
}

7. 总结

7.1 核心要点

  1. 三种句柄必须区分清楚:

    • GroupHandle: 用户定义的组ID
    • ClientHandle: 用户定义的变量ID
    • ServerHandle: 系统分配的句柄,用于实际读写
  2. ServerHandle 数组从索引1开始:

    • 创建时长度 = 变量数 + 1
    • 访问时索引 = 变量索引 + 1
  3. 异步操作需要事件回调:

    • 异步读取: OnReadCompleted
    • 数据订阅: OnDataChanged
    • 异步写入: AsyncWriteComplete(需实现)
  4. 读写时使用 ServerHandle:

    • 通过 ClientHandle 找到变量索引
    • 通过索引获取 ServerHandle
    • 使用 ServerHandle 调用读写方法

7.2 最佳实践

  1. 保持 ClientHandle 连续且从0开始
  2. 一个应用只创建一个 OPCServer 实例
  3. 合理设置 UpdateRate 避免频繁读取
  4. 使用订阅替代轮询读取
  5. 正确处理异常和错误码

附录

参考资料

  • OPC DA 规范: OPC Foundation
  • OPCAutomation.dll: OPC DA 2.0/3.0 COM 组件
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐