本文档基于自写的
OPC_Test_Pro项目编写,讲解 OPC DA 通讯的原理、句柄关系及每个方法的实现细节。本文只用于对该项目的技术分析。项目为自用项目,源码不对外开源。
目录
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
核心属性:
csharp
private OPCServer opcServer; // OPC服务器对象
private List<Array> serverHandleList; // 服务器句柄列表(重要!)
public List<OPCDAGroup> OPCGroupList; // OPC组列表
核心事件:
csharp
public event EventHandler<OPCDAAsyncEventArgs> OnReadCompleted; // 异步读取完成事件
public event EventHandler<OPCDAAsyncEventArgs> OnDataChanged; // 数据订阅变化事件
2.2 OPCDAGroup - OPC组定义
位置 : Related_OPCLib/OPCDAGroup.cs
csharp
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
csharp
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):
csharp
OPCDAGroup group = new OPCDAGroup()
{
GroupName = "Group1",
IsActive = true,
GroupHandle = 0, // 用户自定义组ID
};
3.2.2 ClientHandle(项客户端句柄)
- 定义者: 用户(程序员)
- 用途: 唯一标识一个 OPC 变量(项)
- 值范围: 用户自定义,通常是 0, 1, 2...(对应变量在数组中的索引)
- 重要特性: 必须在同一个组内唯一
- 使用场景 :
- 异步读取时匹配返回的数据
- 写入时指定要写入的变量
示例 (FrmOPCDA.cs:130-136):
csharp
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):
csharp
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):
csharp
// 通过 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
原理:
csharp
1. 获取本机IP地址列表
2. 通过DNS反向查询获取主机名
3. 过滤并去重,返回可用的OPC服务器节点
返回示例:
["DESKTOP-ABC123", "192.168.1.100"]
4.1.2 GetOPCServerNames - 获取OPC服务器名称列表
位置: OPCDALib.cs:54-73
原理:
csharp
1. 创建 OPCServer 对象
2. 调用 GetOPCServers() 获取本机所有OPC服务器
3. 返回服务器名称列表
返回示例:
["Kepware.KEPServerEX.V6", "OPC.SimaticNET"]
4.1.3 Connect - 建立OPC连接
位置: OPCDALib.cs:81-98
参数:
serverNode: 服务器节点(IP或主机名)serverName: OPC服务器名称
原理:
csharp
1. 创建新的 OPCServer 实例
2. 调用 Connect(serverName, serverNode) 连接到OPC服务器
3. 成功返回 OperateResult.CreateSuccessResult()
4. 失败捕获异常并返回错误信息
调用示例:
csharp
var result = objOPC.Connect("DESKTOP-ABC123", "Kepware.KEPServerEX.V6");
4.2 变量管理方法
4.2.1 GetOPCBrower - 获取OPC变量列表
位置: OPCDALib.cs:115-131
原理:
csharp
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组列表
原理详解:
csharp
1. 保存 OPCGroupList 到类属性(供后续读写使用)
2. 清空所有旧的OPC组
3. 设置默认属性:
- DefaultGroupDeadband = 0 (死区为0,任何变化都触发)
- DefaultGroupIsActive = true (组默认激活)
4. 遍历每个组,调用 AddOPCGroup() 添加
5. 返回操作结果
重要代码 (OPCDALib.cs:152):
csharp
serverHandleList = new List<Array>(); // 清空并重新初始化
4.2.3 AddOPCGroup - 添加单个OPC组(私有方法)
位置: OPCDALib.cs:171-214
原理详解:
csharp
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):
csharp
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
原理:
csharp
遍历每个 OPC 组:
1. 获取该组的 ServerHandle 数组
2. 调用 SyncRead() 同步读取
3. 参数说明:
- Source: 1 (从缓存读取)
- ItemCount: 变量数量
- ServerHandles: 服务器句柄数组
4. 返回读取到的值数组
5. 转换为 object[] 并添加到结果列表
调用示例 (FrmOPCDA.cs:178):
csharp
List<object[]> value = this.objOPC.SyncRead();
// value[0] 包含第一个组所有变量的值
4.3.2 AsyncRead - 异步读取
位置: OPCDALib.cs:327-345
原理:
csharp
遍历每个 OPC 组:
1. 获取该组的 ServerHandle 数组
2. 调用 AsyncRead() 发起异步读取请求
3. 参数说明:
- ItemCount: 变量数量
- ServerHandles: 服务器句柄数组
- Errors: 返回错误码数组
- TransactionID: 设为 GroupHandle(用于回调时识别)
- CancelID: 返回的取消ID
4. 读取完成后触发 AsyncReadComplete 事件
事件处理 (FrmOPCDA.cs:213-239):
csharp
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
原理:
csharp
1. 订阅 OnDataChanged 事件
2. 服务器自动检测数据变化
3. 变化时触发 DataChange 事件
4. 无需主动调用读取方法
事件处理 (FrmOPCDA.cs:258-281):
csharp
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: 项句柄
原理详解:
csharp
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):
csharp
// 找到 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):
csharp
// 正确示例: 使用变量下拉框的 SelectedIndex
var result = this.objOPC.SyncWrite(
this.txt_SetValue.Text.Trim(), // 写入的值
0, // GroupHandle
this.cmb_SetVariable.SelectedIndex // ClientHandle (0, 1, 2...)
);
❌ 错误示例:
csharp
// 错误: 使用了服务器名称下拉框的索引
var result = this.objOPC.SyncWrite(
"100",
0,
this.cmb_ServerName.SelectedIndex // 这是服务器索引,不是变量句柄!
);
// 结果: 找不到对应的 ClientHandle,导致索引超限错误
4.5.2 AsyncWrite - 异步写入
位置: OPCDALib.cs:389-416
参数: 与 SyncWrite 相同
原理:
csharp
1. 参数准备与 SyncWrite 完全相同
2. 调用 AsyncWrite() 而非 SyncWrite():
AsyncWrite(1, serverHandle, values, out errors, transactionID, out cancelID)
3. 写入完成后触发 AsyncWriteComplete 事件
差异对比:
| 特性 | SyncWrite | AsyncWrite |
|---|---|---|
| 阻塞等待 | ✅ 是 | ❌ 否 |
| 返回时机 | 写入完成后返回 | 立即返回 |
| 适用场景 | 单次少量写入 | 批量写入 |
| 事件回调 | 无 | AsyncWriteComplete |
5. 完整使用流程
5.1 初始化流程
csharp
// 步骤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 读取流程
csharp
// 方式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 订阅流程
csharp
// 开启订阅
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 写入流程
csharp
// 同步写入
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 断开连接
csharp
objOPC.DisConnect();
6. 常见问题
6.1 索引超限错误
问题 : 调用 SyncWrite() 或 AsyncWrite() 时报"索引超限"错误
原因:
- 传入了错误的
clientHandle - 通常混淆了下拉框的
SelectedIndex
解决方案:
csharp
// ❌ 错误
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保留未使用
代码处理:
csharp
// 创建数组时长度 = 变量数 + 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组?
csharp
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 事件为空
建议添加:
csharp
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 核心要点
-
三种句柄必须区分清楚:
- GroupHandle: 用户定义的组ID
- ClientHandle: 用户定义的变量ID
- ServerHandle: 系统分配的句柄,用于实际读写
-
ServerHandle 数组从索引1开始:
- 创建时长度 = 变量数 + 1
- 访问时索引 = 变量索引 + 1
-
异步操作需要事件回调:
- 异步读取: OnReadCompleted
- 数据订阅: OnDataChanged
- 异步写入: AsyncWriteComplete(需实现)
-
读写时使用 ServerHandle:
- 通过 ClientHandle 找到变量索引
- 通过索引获取 ServerHandle
- 使用 ServerHandle 调用读写方法
7.2 最佳实践
- 保持 ClientHandle 连续且从0开始
- 一个应用只创建一个 OPCServer 实例
- 合理设置 UpdateRate 避免频繁读取
- 使用订阅替代轮询读取
- 正确处理异常和错误码
附录
参考资料
- OPC DA 规范: OPC Foundation
- OPCAutomation.dll: OPC DA 2.0/3.0 COM 组件