PLC通讯实现-C#访问OpcServer实现读写PLC(九)
背景
由于工厂设备种类多、分阶段建设,工控程序开发通常面临对接多种PLC厂商设备和不同系列与型号。因此出现了一种专门与不同PLC通讯的软件协议-OPC(OLE for Process Control),进而有一些公司开发了基于OPC协议的软件,比如KEPServerEX(付费软件),目的是简化工控程序开发时与PLC通讯的过程,我们只需要按一种协议与OpcServer通讯就可以了。下面就介绍一下使用C#与OpcServer通讯的方法步骤。
依赖
我们通常不会从头写,可以基于OpcDaNet.dll库或Interop.OPCAutomation.dll库,基于OPCAutomation的例子有很多,本文我们就以OpcDaNet库为例讲解,而且附上OpcDaNet.dll的源代码。
配置OpcServer
首先我们需要配置OpcServer,本例我们使用了KEPServerExV5.14,因为是试用版,需要每隔一段时间重启一次服务,不过不影响我们学习和测试。
1、打开KEPServerEX,新建一个通道,此处我们命名为chnlSiemens。
2、在此chnlSiemens通道下新建一个设备,此处命名为S7-300,根据向导连接PLC,我使用的是西门子S7 300的PLC,并且在PLC中开了2个数据块,分别为DB4长度110个字、DB5长度122个字。
3、在S7-300设备下,按照PLC的实际数据块创建标签组,标签组的名称分别为DB4和DB5。
4、在DB4标签组下创建2个标签,第一个名称为0-99,地址为DB4DBW0.100,数据类型为Short,第二个名称为100-109,地址为DB4DBW100.10,数据类型为Short,即定义长度最长为100的Short数组。方便快速读取。
5、在DB5标签组下创建3个标签,第一个名称为0-99,地址为DB5DBW0.100,数据类型为Short,第二个名称为100-121, 地址为DB5DBW100.22,数据类型为Short,即定义长度最长为100的Short数组。方便快速读取。第三个标签名称为DB5DBW64,地址为DB5DBW64,数据类型为Short。
具体如下图:
6、可以点击工具栏上的Quick Client,打开客户端监视一下标签的值,如下图:
C#程序实现与OPCServer通讯
1、封装Equip.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OPC;
using OPCDA;
using OPCDA.NET;
using Mesnac.Equips;
namespace Mesnac.Equip.OPC.OpcDaNet.OPC
{
public class Equip : BaseEquip
{
#region 字段定义
private bool _isOpen = false;
private OpcServer myOPCServer;
private DataChangeEventHandler dch; //数据刷新委托对象
private RefreshGroup asyncRefrGroup; //数据刷新组,把要感知数据刷新的标签加入此组,这样标签值变化时才会触发DataChange事件
private SyncIOGroup readWriteGroup; //数据读写组,把要进行写入操作的标签放入词组,调用Write方法才会生效
private Dictionary<string, object> readResult = null; //设备标签数据缓存
private int stepLen = 100; //标签变量的步长设置
private string groupNamePrefix = "DB"; //数据块号前缀
#endregion
#region 属性定义
/// <summary>
/// OPCServer IP地址
/// </summary>
public string OpcServerIP
{
get
{
return "192.168.1.105";
}
}
/// <summary>
/// OPC服务名称
/// </summary>
public string OpcServerName
{
get
{
return "Kepware.KEPServerEX.V5";
}
}
/// <summary>
/// 通道名称
/// </summary>
public string ChannelName
{
get
{
return "chnlSiemens";
}
}
/// <summary>
/// 设备名称
/// </summary>
public string DeviceName
{
get
{
return "S7-300";
}
}
#endregion
public override bool Open()
{
lock (this)
{
if (this._isOpen == true && this.myOPCServer != null)
{
return true;
}
this.State = false;
this.myOPCServer = new OpcServer();
int res = this.myOPCServer.Connect(this.OpcServerIP, this.OpcServerName); //连接OPCServer
if (HRESULTS.Failed(res))
{
this.myOPCServer = null;
Console.WriteLine("OPC连接失败:" + res);
this.State = false;
return false;
}
else
{
this.State = true;
this._isOpen = true;
Console.WriteLine("OPC连接成功!");
this.readWriteGroup = new SyncIOGroup(this.myOPCServer);
dch = new DataChangeEventHandler(DataChangeHandler);
this.asyncRefrGroup = new RefreshGroup(myOPCServer, dch, this.Main.ReadHz);
#region 初始化读取结果
this.readResult = new Dictionary<string, object>();
foreach (Equips.BaseInfo.Group group in this.Group.Values)
{
int tagCount = group.Len % this.stepLen == 0 ? group.Len / this.stepLen : group.Len / this.stepLen + 1;
int currLen = 0;
for(int i = 0;i < tagCount ;i++)
{
string tagName = String.Empty;
if (tagCount == 1)
{
tagName = String.Format("{0}-{1}", group.Start, group.Start + group.Len - 1);
currLen = group.Len;
}
else if (i == tagCount - 1)
{
tagName = String.Format("{0}-{1}", group.Start + (i * this.stepLen), group.Start + (i * this.stepLen) + group.Len % this.stepLen - 1);
currLen = group.Len % this.stepLen;
}
else
{
tagName = String.Format("{0}-{1}", group.Start + (i * this.stepLen), group.Start + (i * this.stepLen) + this.stepLen - 1);
currLen = this.stepLen;
}
string tagFullName = String.Format("{0}.{1}", group.Name, tagName);
if (!this.readResult.ContainsKey(tagFullName))
{
short[] groupData = new short[currLen];
this.readResult[tagFullName] = groupData;
this.Add2RefrGroup(String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, tagFullName));
}
}
}
#endregion
}
return this.State;
}
}
public override bool Read(string block, int start, int len, out object[] buff)
{
lock (this)
{
buff = null;
try
{
if (!Open())
{
return false;
}
string startTag = String.Empty;
string groupName = String.Format("{0}{1}", this.groupNamePrefix, block); //要读取的OPCServer块
List<short> groupData = new List<short>();
foreach (string key in this.readResult.Keys)
{
if (key.StartsWith(groupName) && key.Replace(String.Format("{0}.", groupName), String.Empty).Contains("-"))
{
if (String.IsNullOrEmpty(startTag))
{
startTag = key.Replace(String.Format("{0}.", groupName), String.Empty);
}
short[] values = this.readResult[key] as short[];
groupData.AddRange(values);
}
}
buff = new object[len];
int startIndex = 0;
string strStartIndex = startTag.Substring(0, startTag.IndexOf("-"));
int.TryParse(strStartIndex, out startIndex);
startIndex = start - startIndex;
Array.Copy(groupData.ToArray(), startIndex, buff, 0, buff.Length);
return true;
}
catch (Exception ex)
{
Console.WriteLine(this.Name + "读取失败:" + ex.Message);
this.State = false;
return false;
}
}
}
public override bool Write(int block, int start, object[] buff)
{
lock (this)
{
try
{
if (!Open())
{
return false;
}
bool isWrite = false;
#region 按标签变量写入
string itemId = "";
foreach (Equips.BaseInfo.Group group in this.Group.Values)
{
if (group.Block == block.ToString())
{
foreach (Equips.BaseInfo.Data data in group.Data.Values)
{
if (data.Start == start && data.Len == buff.Length)
{
itemId = String.Format("{0}.{1}.{2}{3}.{4}", this.ChannelName, this.DeviceName, this.groupNamePrefix, block, data.Name);
break;
}
}
}
}
if (!String.IsNullOrEmpty(itemId))
{
if (this.AddItem(itemId) == 0)
{
ItemDef itemData = this.readWriteGroup.Item(itemId);
if (itemData != null)
{
int res = 0;
if (buff.Length == 1)
{
res = this.readWriteGroup.Write(itemData, buff[0]);
}
else
{
res = this.readWriteGroup.Write(itemData, buff);
}
string error = readWriteGroup.GetErrorString(res);
if (res != 0)
{
Console.WriteLine(String.Format("标签变量[{0}]写入失败:{1}", itemId, error));
return false;
}
else
{
isWrite = true;
}
}
}
}
if (isWrite)
{
return true;
}
#endregion
#region 按块写入
#region 先读取相应标签数数据
string startTag = String.Empty;
string groupName = String.Format("{0}{1}", this.groupNamePrefix, block); //要读取的OPCServer块
List<short> groupData = new List<short>();
foreach (string key in this.readResult.Keys)
{
if (key.StartsWith(groupName) && key.Replace(String.Format("{0}.", groupName), String.Empty).Contains("-"))
{
if (String.IsNullOrEmpty(startTag))
{
startTag = key.Replace(String.Format("{0}.", groupName), String.Empty);
}
string[] beginEnd = key.Replace(String.Format("{0}.", groupName), String.Empty).Split(new char[] { '-' });
if (beginEnd.Length != 2)
{
Console.WriteLine(String.Format("标签变量[{0}]未按约定方式命名,请按[起始字-结束字]方式标签变量进行命名!", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key)));
return false;
}
int begin = 0;
int end = 0;
int.TryParse(beginEnd[0], out begin);
int.TryParse(beginEnd[1], out end);
#region 写入之前,先读取一下PLC的值
if ((start >= begin && start <= end) || ((start + buff.Length - 1) >= begin && (start + buff.Length - 1) <= end))
{
ItemDef itemData = this.readWriteGroup.Item(String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key));
if (itemData == null)
{
Console.WriteLine(String.Format("标签变量[{0}]未添加到数据读写组中!", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key)));
return false;
}
OPCItemState itemState = null;
int res = this.readWriteGroup.Read(OPCDATASOURCE.OPC_DS_DEVICE, itemData, out itemState);
if (HRESULTS.Failed(res))
{
string error = this.readWriteGroup.GetErrorString(res);
Console.WriteLine(String.Format("读取标签变量[{0}]的值失败:{1}", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key), error));
return false;
}
if (itemState.DataValue is Array)
{
groupData.AddRange(itemState.DataValue as short[]);
}
else
{
Console.WriteLine(String.Format("标签变量[{0}]的长度未指定!", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key)));
}
}
#endregion
}
}
#endregion
#region 更新标签中对应的数据后,再写回OPCServer
int startIndex = 0;
string strStartIndex = startTag.Substring(0, startTag.IndexOf("-"));
int.TryParse(strStartIndex, out startIndex);
startIndex = start - startIndex;
short[] newDataBuffer = groupData.ToArray();
for (int i = 0; i < buff.Length; i++)
{
short svalue = 0;
short.TryParse(buff[i].ToString(), out svalue);
newDataBuffer[startIndex + i] = svalue;
}
int index = 0;
foreach (string key in this.readResult.Keys)
{
if (key.StartsWith(groupName) && key.Replace(String.Format("{0}.", groupName), String.Empty).Contains("-"))
{
string[] beginEnd = key.Replace(String.Format("{0}.", groupName), String.Empty).Split(new char[] { '-' });
if (beginEnd.Length != 2)
{
Console.WriteLine(String.Format("标签变量[{0}]未按约定方式命名,请按[起始字-结束字]方式标签变量进行命名!", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key)));
return false;
}
int begin = 0;
int end = 0;
int.TryParse(beginEnd[0], out begin);
int.TryParse(beginEnd[1], out end);
if ((start >= begin && start <= end) || ((start + buff.Length - 1) >= begin && (start + buff.Length - 1) <= end))
{
ItemDef itemData = this.readWriteGroup.Item(String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key));
if (itemData == null)
{
Console.WriteLine(String.Format("写入失败:标签变量[{0}]未添加到数据读写组中!", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key)));
return false;
}
if (!(this.readResult[key] is Array))
{
Console.WriteLine(String.Format("标签变量[{0}]的长度未指定!", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key)));
return false;
}
int len = (this.readResult[key] as short[]).Length;
short[] tagDataBuff = new short[len];
Array.Copy(newDataBuffer, index, tagDataBuff, 0, tagDataBuff.Length);
index += tagDataBuff.Length;
int res = this.readWriteGroup.Write(itemData, tagDataBuff);
if (HRESULTS.Failed(res))
{
string error = this.readWriteGroup.GetErrorString(res);
Console.WriteLine(String.Format("向标签变量[{0}]中写入值失败:{1}", String.Format("{0}.{1}.{2}", this.ChannelName, this.DeviceName, key), error));
return false;
}
else
{
Console.WriteLine("写入...");
}
}
}
}
#endregion
#endregion
return true;
}
catch (Exception ex)
{
Console.WriteLine(this.Name + "写入失败:" + ex.Message);
return false;
}
}
}
public override void Close()
{
lock (this)
{
if (this.myOPCServer != null)
{
if (this.asyncRefrGroup != null)
{
this.asyncRefrGroup.Dispose();
}
if (this.readWriteGroup != null)
{
this.readWriteGroup.Dispose();
}
this.myOPCServer.Disconnect();
System.Threading.Thread.Sleep(2000);
this.myOPCServer = null;
}
}
}
#region 辅助方法
/// <summary>
/// OPCServer数据更新事件处理方法
/// </summary>
/// <param name="sender">事件源,一般为标签组</param>
/// <param name="e">事件参数</param>
private void DataChangeHandler(object sender, OPCDA.NET.DataChangeEventArgs e)
{
OPCDA.NET.OPCItemState[] itemStates = e.sts;
foreach (OPCDA.NET.OPCItemState itemState in itemStates)
{
OPCDA.NET.ItemDef itemDef = this.asyncRefrGroup.FindClientHandle(itemState.HandleClient);
if (itemDef != null)
{
this.readResult[itemDef.OpcIDef.ItemID.Replace(String.Format("{0}.{1}.", this.ChannelName, this.DeviceName), String.Empty)] = itemState.DataValue; //把最新数据放入读取结果中
}
}
}
/// <summary>
/// 向数据读写组和数据刷新组中添加标签变量
/// </summary>
/// <param name="itemId">变量ID</param>
/// <returns>成功返回0,失败返回-1</returns>
private int Add2RefrGroup(string itemId)
{
if (AddItem(itemId) == 0) //数据读写组
{
ItemDef itemData = this.readWriteGroup.Item(itemId);
int res = this.asyncRefrGroup.Add(itemData.OpcIDef.ItemID); //数据刷新组
if (HRESULTS.Failed(res))
{
Console.WriteLine(String.Format("向数据更新组中添加标签变量[{0}]失败,请检查OPCServer中有没有配置此标签!", itemId));
return -1;
}
return 0;
}
return -1;
}
/// <summary>
/// 向数据读写组添加标签变量
/// </summary>
/// <param name="itemId">变量ID</param>
/// <returns>成功返回0, 失败返回-1</returns>
private int AddItem(string itemId)
{
ItemDef itemData = this.readWriteGroup.Item(itemId);
if (itemData == null)
{
this.readWriteGroup.Add(itemId);
itemData = this.readWriteGroup.Item(itemId);
if (itemData == null)
{
Console.WriteLine(String.Format("向数据读写组中添加标签变量[{0}]失败,请检查OPCServer中有没有配置此标签!", itemId));
return -1;
}
}
return 0;
}
#endregion
}
}
2、测试代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
//using Mesnac.Equip.OMRON.HostLink.COM;
//using Mesnac.Equip.Mitsubishi.MXComponent.Default;
//using Mesnac.Equip.Siemens.S7_300.Net;
//using Mesnac.Equip.Siemens.S7_1500.Ethernet;
using Mesnac.Equip.OPC.OpcDaNet.OPC;
namespace TestWinApp
{
public partial class Form1 : Form
{
private Equip thisEquip = new Equip();
private OPCDA.NET.OpcServer opcServer = new OPCDA.NET.OpcServer();
private OPCDA.NET.DataChangeEventHandler dch;
private OPCDA.NET.RefreshGroup refreshGroup = null;
private OPCDA.NET.SyncIOGroup readWriteGroup = null;
private Dictionary<string, object> lastData = new Dictionary<string,object>();
public Form1()
{
InitializeComponent();
}
//连接设备
private void button1_Click(object sender, EventArgs e)
{
try
{
this.thisEquip.Main.ReadHz = 1000;
this.thisEquip.Group.Clear();
Mesnac.Equips.BaseInfo.Group group4 = new Mesnac.Equips.BaseInfo.Group();
group4.Name = "DB4";
group4.Block = "4";
group4.Start = 0;
group4.Len = 110;
this.thisEquip.Group.Add(group4.Name, group4);
Mesnac.Equips.BaseInfo.Group group5 = new Mesnac.Equips.BaseInfo.Group();
group5.Name = "DB5";
group5.Block = "5";
group5.Start = 0;
group5.Len = 122;
Mesnac.Equips.BaseInfo.Data DB5DBW64 = new Mesnac.Equips.BaseInfo.Data();
DB5DBW64.Name = "DB5DBW64";
DB5DBW64.Start = 32;
DB5DBW64.Len = 1;
group5.Data.Add("DB5DBW64", DB5DBW64);
this.thisEquip.Group.Add(group5.Name, group5);
this.thisEquip.Open();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
MessageBox.Show(ex.StackTrace);
}
}
//读取测试
private void button2_Click(object sender, EventArgs e)
{
try
{
string block = this.textBox1.Text;
int start = 0;
int.TryParse(this.textBox2.Text,out start);
int len = 0;
int.TryParse(this.textBox3.Text,out len);
object[] buff = new object[len];
bool result = this.thisEquip.Read(block, start, len, out buff);
if (result)
{
StringBuilder sb = new StringBuilder();
foreach(object obj in buff)
{
sb.Append(obj.ToString()).Append(",");
}
this.textBox5.Text = sb.ToString();
MessageBox.Show("读取成功!");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
MessageBox.Show(ex.StackTrace);
}
}
//写入测试
private void button3_Click(object sender, EventArgs e)
{
try
{
int block = 0;
int.TryParse(this.textBox1.Text, out block);
int start = 0;
int.TryParse(this.textBox2.Text, out start);
int len = 0;
int.TryParse(this.textBox3.Text, out len);
object[] buff = new object[len];
for (int i = 0; i < len; i++)
{
buff[i] = Convert.ToInt32(this.textBox4.Text);
}
bool result = this.thisEquip.Write(block, start,buff);
if (result)
{
MessageBox.Show("写入成功!");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
MessageBox.Show(ex.StackTrace);
}
}
//关闭连接
private void button4_Click(object sender, EventArgs e)
{
try
{
this.thisEquip.Close();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
}
3、运行界面如下