最近一个项目,需要跟PLC通讯,所以测试使用了OPC server。现主要记录使用C#编写的Client例程,其它方面不作详细描述。
第一步,OPC Server使用的是KEPServer 5版本,网上很多资料。安装完成后,它的配置页面如下图。配置中,我已配置了和Omron PLC连接的project,创建了访问PLC的area地址的几十个变量。具体配置根据不同PLC的信息对应配置就行了。
第二步,开始编写C#程序。因为我的代码是嵌套在现有的项目上的,所以创建了一个类来实现。大概的流程就是软件开启->创建与OPC Server通讯的Client线程。线程方法即为循环判断通讯是否有掉线,若掉线则断开重新连接。首先要在项目添加OPC的dll引用 Interop.OPCAutomation.dll
1、创建线程
#region OPC通讯线程
try
{
OPCClient opcClient = new OPCClient();
Thread thrOpc = new Thread(opcClient.OPCClientOperate);
thrOpc.IsBackground = true;
thrOpc.Start();
}
catch { }
#endregion
2、创建类
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using OPCAutomation;
using Model;
using System.Threading;
namespace BLL
{
public class OPCClient
{
#region 全局变量
/// <summary>
/// OPC对应PLC的位置
/// </summary>
public static Dictionary<string, OPCItemParameter> dtOpcToPlc = new Dictionary<string, OPCItemParameter>();
/// <summary>
/// opc服务器信息
/// </summary>
public static OPCInformation opcInformation = new OPCInformation();
#endregion
/// <summary>
/// 初始化
/// </summary>
public OPCClient()
{ }
}
}
上述中的dtOpcToPlc为后台配置,用于配置对应在OPC Server中想获取的变量的名称及对应的属性,因为项目实施时可能有变动,对于信息的获取只能用配置的形式了。
OPCInformation则是创建的与OPC通讯的信息实例了
两个实例如下
/// <summary>
/// opc参数信息
/// </summary>
public class OPCItemParameter
{
public OPCItemParameter()
{
this.ChangeTime = DateTime.Now;
}
/// <summary>
/// 初始化
/// </summary>
/// <param name="pName">opc项名称</param>
/// <param name="plcName">PLC命名</param>
/// <param name="handle">客户端句柄</param>
/// <param name="value">值</param>
public OPCItemParameter(string pName, string plcName, int handle, int value)
{
this.ParameterName = pName;
this.PLCName = plcName;
this.ItemHandle = handle;
this.Value = value;
this.ChangeTime = DateTime.Now;
this.IsWriteOk = false;
}
/// <summary>
/// 客户端参数句柄
/// </summary>
public int ItemHandle { get; set; }
/// <summary>
/// 对应PLC值的参数名称(OPC server命名)
/// </summary>
public string ParameterName { get; set; }
/// <summary>
/// PLC位置名称
/// </summary>
public string PLCName { get; set; }
/// <summary>
/// 参数值
/// </summary>
public int Value { get; set; }
/// <summary>
/// 品质
/// </summary>
public string Qualities { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public string TimeStamps { get; set; }
/// <summary>
/// 值发生变化的时间,用于后期任务优先级
/// </summary>
public DateTime ChangeTime { get; set; }
/// <summary>
/// 是否写入成功
/// </summary>
public bool IsWriteOk { get; set; }
}
#region OPC服务器类信息
/// <summary>
/// OPC服务器的参数信息
/// </summary>
public class OPCInformation
{
public OPCInformation()
{
this.Ip = string.Empty;
this.HostName = string.Empty;
this.ConnectState = false;
this.GroupsState = false;
this.ConnectContents = "Opc Failed";
}
/// <summary>
/// ip地址
/// </summary>
public string Ip { get; set; }
/// <summary>
/// 名称
/// </summary>
public string HostName { get; set; }
/// <summary>
/// opc服务器名称
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// 服务器句柄
/// </summary>
public int itmHandleServer { get; set; }
/// <summary>
/// opc服务器对象
/// </summary>
public OPCServer KepServer { get; set; }
/// <summary>
/// opc组别集合对象
/// </summary>
public OPCGroups KepGroups { get; set; }
/// <summary>
/// opc组别对象
/// </summary>
public OPCGroup KepGroup { get; set; }
/// <summary>
/// opc项集合对象
/// </summary>
public OPCItems KepItems { get; set; }
/// <summary>
/// opc项对象
/// </summary>
public OPCItem KepItem { get; set; }
/// <summary>
/// 连接状态
/// </summary>
public bool ConnectState { get; set; }
/// <summary>
/// 连接内容
/// </summary>
public string ConnectContents { get; set; }
/// <summary>
/// 创建群组是否成功
/// </summary>
public bool GroupsState { get; set; }
}
#endregion
3、下面为创建连接通讯及循环判断是否掉线,这个主要是为了新创建连接及掉线是能迅速响应重连
/// <summary>
/// 对opc获取的数据进行业务处理
/// </summary>
public void OPCClientOperate()
{
int lineoffCount = 0;//掉线判断计数
while (true)
{
try
{
if (!opcInformation.ConnectState)
{//连接不成功,尝试重新连接
if (GetLocalServer())
{//获取OPC服务器信息成功
if (ConnectRemoteServer())
{//连接OPC成功
opcInformation.ConnectState = true;
RecurBrowse(opcInformation.KepServer.CreateBrowser());
}
}
else
{
Thread.Sleep(3000);
}
}
else
{
if (!opcInformation.GroupsState)
{//创建组集合失败,尝试重新创建
opcInformation.GroupsState = CreateGroup();
}
else
{
//判断状态及时重连
}
}
}
catch (Exception ex)
{
opcInformation.ConnectState = false;
opcInformation.ConnectContents = "OPC disconnected";
opcInformation.GroupsState = false;
try
{
opcInformation.KepServer.Disconnect();
}
catch { }
}
//Thread.Sleep(100);
}
}
4、当连接上时,OPC的dll控件有数据变化响应事件创建调用就行了
/// <summary>
/// 每当项数据有变化时执行的事件
/// </summary>
/// <param name="TransactionID">处理ID</param>
/// <param name="NumItems">项个数</param>
/// <param name="ClientHandles">项客户端句柄</param>
/// <param name="ItemValues">TAG值</param>
/// <param name="Qualities">品质</param>
/// <param name="TimeStamps">时间戳</param>
private void KepGroup_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
{
for (int i = 1; i <= NumItems; i++)
{
try
{
int index = int.Parse(ClientHandles.GetValue(i).ToString());
string key = dtOpcToPlc.FirstOrDefault(o => o.Value.ItemHandle == index).Key;
if (!string.IsNullOrEmpty(key))
{//需要判断类型,是int还是boolean
int value = int.Parse(ItemValues.GetValue(i).ToString());
if (value != dtOpcToPlc[key].Value)
{
dtOpcToPlc[key].Value = value;
dtOpcToPlc[key].ChangeTime = DateTime.Now;
}
dtOpcToPlc[key].Qualities = Qualities.GetValue(i).ToString();
dtOpcToPlc[key].TimeStamps = TimeStamps.GetValue(i).ToString();
}
}
catch { }
}
}
5、向OPC Server的变量写入数据
/// <summary>
/// 向OPC对应项写入值
/// </summary>
/// <param name="value">需要写入的值</param>
/// <param name="OPCItemParameter">item地址</param>
/// <returns></returns>
public static bool WriteOpc(int value, OPCItemParameter opcItem)
{
try
{
string key = dtOpcToPlc.First(o => o.Value.ItemHandle == opcItem.ItemHandle).Key;
dtOpcToPlc[key].IsWriteOk = false;
OPCItem bItem = opcInformation.KepItems.Item(opcItem.ParameterName);
opcInformation.itmHandleServer = bItem.ServerHandle;
int[] temp = new int[2] { 0, bItem.ServerHandle };
Array serverHandles = (Array)temp;
object[] valueTemp = new object[2] { "", value.ToString() };
Array values = (Array)valueTemp;
Array Errors;
int cancelID;
opcInformation.KepGroup.AsyncWrite(1, ref serverHandles, ref values, out Errors, 2009, out cancelID);
//KepItem.Write(txtWriteTagValue.Text);//这句也可以写入,但并不触发写入事件
GC.Collect();
return true;
}
catch
{
return false;
}
}
很简单,只需要调用对应的函数就可以了。
6、写入成功响应
当写入成功后,对应的响应函数会响应
private void KepGroup_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors)
{
try
{
for (int i = 1; i <= NumItems; i++)
{
int error = int.Parse(Errors.GetValue(i).ToString());
int handle = int.Parse(ClientHandles.GetValue(i).ToString());
string key = dtOpcToPlc.First(o => o.Value.ItemHandle == handle).Key;
if (error == 0)
{
dtOpcToPlc[key].IsWriteOk = true;
}
}
}
catch { }
}
其它的一些基本连接方法(ConnectRemoteServer、GetLocalServer、RecurBrowse、SetGroupProperty等),是引用了百度上其它网友的案例,就不一一描述了。
7、总结
OPC的dll提供了很多接口,相对调用简单,只需要根据项目来作简单修改。对于掉线异常重连,则需要根据实际调试案例来处理就行了,这个需要花一些时间来测试。
这案例只测试了Omron PLC的通讯连接,其它PLC尚未进行实际测试。