最近一个项目,需要跟PLC通讯,所以测试使用了OPC server。现主要记录使用C#编写的Client例程,其它方面不作详细描述。

  第一步,OPC Server使用的是KEPServer 5版本,网上很多资料。安装完成后,它的配置页面如下图。配置中,我已配置了和Omron PLC连接的project,创建了访问PLC的area地址的几十个变量。具体配置根据不同PLC的信息对应配置就行了。

Codesys做OPCUA通讯_Codesys做OPCUA通讯

  第二步,开始编写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尚未进行实际测试。