C#之OPC通信
1、OPC通信概述
OPC是OLE for Process Control的缩写,即应用于过程控制的OLE。
OPC服务器由三类对象组成,相当于三种层次上的接口:服务器、组对象和数据项。
服务器对象包含服务器的所有信息,同时也是组对象的容器。一个服务器对应于一个OPC服务器,即一种设备的驱动程序。在一个服务器中,可以有若干个组。
组对象包含本组的所有信息,同时包含并管理OPC数据项。OPC组对象为客户提供了组织数据的一种方法。组是应用程序组织数据的一个单位,客户可对其进行读写,还可设置客户端的数据更新速率。当服务器缓冲区内数据发生改变时,OPC服务器将向客户发出通知,客户得到通知后再进行必要的处理,而无需浪费大量的时间进行查询。OPC规范定义了两种组对象:公共组(或称全局组,public)和局部组(或称局域组、私有组,local)。公共组由多个客户共有,局域组只隶属于一个OPC客户。全局组对所有连接在服务器上的应用程序都有效、而局域组只能对建立它的Client有效。一般说来,客户和服务器的一对连接只需要定义一个组对象。在一个组中,可以有若干个数据项。
数据项是读写数据的最小逻辑单位,一个数据项与一个具体的位号相连。数据项不能独立于组存在,必须隶属于某一个组。在每个组对象中,客户可以加入多个OPC数据项。
OPC数据项是服务器端定义的对象,通常指向设备的一个寄存器单元。OPC客户对设备寄存器的操作都是通过其数据项来完成的。通过定义数据项,OPC规范尽可能地隐藏了设备的特殊信息,也使OPC服务器的通用性大大增强。OPC数据项并不提供对外接口,客户不能直接对其进行操作,所有操作都是通过组对象进行的。
应用程序作为OPC接口中的Client方,硬件驱动程序作为OPC接口中的服务器方,每一个OPC Client应用程序都可以连接若干个OPC服务器,每一个硬件驱动程序可以为若干个应用程序提供数据。
读取方式:同步和异步
OPC客户和OPC服务器进行数据交互可以有两种不同方式,即同步方式和异步方式。同步方式实现较为简单,当客户数目较少而且同服务器交互的数据量也比较少的时候可以采用这种方式;异步方式实现较为复杂,需要在客户程序中实现服务器回调函数。然而当有大量客户和大量数据交互时,异步方式的效率更高,能够避免客户数据请求的阻塞,并可以最大限度地节省CPU和网络资源。
2、通过KEPServerEX6读取数据
准备材料:链接:https://pan.baidu.com/s/1g-RXG0qBERfc_ct-xipgnA
提取码:58jb
下载并安装好KEPServerEX6、Launch Virtual Serial Port Driver Pro、Modbus Slave。
如下图利用软件Launch Virtual Serial Port Driver Pro给电脑创建两个虚拟串口
利用KEPServerEX6创建一个OPC服务器,设置连接接口为COM2,并添加右边的数据项。
打开Modbus Slave,如下图,并输入一下数据。
双击前两个数据让它自增,点击Connection选择COM1
打开KEPServerEX6的Quick Client
通过KEPServerEx成功读取Modbus Slave中的数据
3、利用OPC的DLL文件编写C#读取客户端
新建项目并将OPC的DLL文件添加到项目的引用中
窗体控件如下:
代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using OPCAutomation;
using System.Net;
namespace CSharp上位机之OPC通信
{
public partial class formMain : Form
{
#region 通信变量
/// <summary>
/// OPC服务器对象
/// </summary>
OPCServer opcServer;
/// <summary>
/// OPC服务器中的变量集
/// </summary>
OPCBrowser opcBrowser;
/// <summary>
/// OPC组集合对象
/// </summary>
OPCGroups opcGroups;
/// <summary>
/// OPC组对象
/// </summary>
OPCGroup opcGroup;
OPCGroup opcGroup1;
/// <summary>
/// OPC多变量添加项目
/// </summary>
OPCItems opcItems;
/// <summary>
/// OPC单变量添加项目数组
/// </summary>
OPCItem[] opcItem;
/// <summary>
/// 变量名数组
/// </summary>
Array itemIDs;
/// <summary>
/// 变量对应的标签数组
/// </summary>
Array serverHandles;
#endregion
#region 其它变量
/// <summary>
/// 主机名
/// </summary>
string strHostName = "";
/// <summary>
/// OPC服务器名
/// </summary>
string strOPCServerName = "";
/// <summary>
/// OPC服务器连接状态
/// </summary>
bool connectState = false;
/// <summary>
/// OPC变量对象包含:变量名称,变量值,读取质量,读取时间
/// </summary>
List<OPCVariable> opcVariableList = new List<OPCVariable>();
#endregion
#region 窗体初始化事件
/// <summary>
/// 窗体初始化事件
/// </summary>
public formMain()
{
InitializeComponent();
}
#endregion
#region 窗体加载事件
/// <summary>
/// 窗体加载事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void formMain_Load(object sender, EventArgs e)
{
//初始化连接状态显示按钮的颜色
this.btnConnectState.BackColor = Color.Red;
//初始化opcItem
opcItem = new OPCItem[4];
}
#endregion
#region 获取客户端名和OPC服务器
/// <summary>
/// 获取客户端名和OPC服务器
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRefresh_Click(object sender, EventArgs e)
{
//获取客户端名称和IP
GetLocaIp();
//获取OPC服务器
GetLocalOPCServer();
//初始化客户端名和OPC服务器名的选择
this.cbClientName.SelectedIndex = 0;
this.cbOPCServerName.SelectedIndex = 0;
}
/// <summary>
/// 获取客户端名称和IP
/// </summary>
/// <param name="hostName"></param>
/// <returns></returns>
private void GetLocaIp()
{
//获取本地计算机名
strHostName = Dns.GetHostName();
this.cbClientName.Items.Add(strHostName);
//获取本机IP地址
try
{
IPHostEntry ipHostEntry = Dns.GetHostEntry(strHostName);
for(int i=0;i<ipHostEntry.AddressList.Length;i++)
{
//从IP地址列表中筛选出IPv4类型的IP地址
//AddressFamily.InterNetwork表示此IP为IPv4,
//AddressFamily.InterNetworkV6表示此地址为IPv6类型
if(ipHostEntry.AddressList[i].AddressFamily==System.Net.Sockets.AddressFamily.InterNetwork)
{
this.cbClientName.Items.Add(ipHostEntry.AddressList[i].ToString());
}
}
}
catch(Exception ex)
{
MessageBox.Show("获取本地IP出错:" + ex.Message);
}
//添加空字符串作为客户端名实现与OPC服务器的连接
if (!this.cbClientName.Items.Contains(""))
{
this.cbClientName.Items.Add("");
}
}
/// <summary>
/// 获取OPC服务器
/// </summary>
private void GetLocalOPCServer()
{
try
{
opcServer = new OPCServer();
object opcServerList = opcServer.GetOPCServers(strHostName);
foreach(string server in (Array)opcServerList)
{
this.cbOPCServerName.Items.Add(server);
}
}
catch(Exception ex)
{
MessageBox.Show("获取OPC服务器出错:" + ex.Message);
}
}
#endregion
#region OPC服务器连接事件
/// <summary>
/// OPC服务器连接事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnConnect_Click(object sender, EventArgs e)
{
if(connectState==false)
{
try
{
//连接选择的OPC服务器
strOPCServerName = this.cbOPCServerName.Text;
opcServer.Connect(strOPCServerName, strHostName);
服务器先行断开
opcServer.ServerShutDown += OPCServerShutDown;
//启动定时器
this.timerUpate.Enabled = true;
this.btnConnect.Text = "断开";
//创建组集合对象
CreateGroups();
}
catch(Exception ex)
{
MessageBox.Show("OPC服务器连接出错:" + ex.Message);
}
}
else
{
opcServer.Disconnect();
this.btnConnect.Text = "连接";
}
}
/// <summary>
/// //服务器先行断开
/// </summary>
/// <param name="Reason"></param>
private void OPCServerShutDown(string Reason)
{
MessageBox.Show("OPC服务器先行断开:" + Reason);
}
/// <summary>
/// 创建组集合
/// </summary>
/// <returns></returns>
private bool CreateGroups()
{
try
{
//获取OPC服务器的组集合
opcGroups = opcServer.OPCGroups;
//获取OPC服务器组集合的组
opcGroup = opcGroups.Add("测试1");//名字可以随便取
opcGroup1 = opcGroups.Add("测试2");
//设置组集合属性
opcGroups.DefaultGroupIsActive = true;//激活组集合
opcGroups.DefaultGroupDeadband = 0;// 死区值,设为0时,服务器端该组内任何数据变化都通知组。
opcGroups.DefaultGroupUpdateRate = 200;// 默认组集合的刷新频率为200ms
//设置组属性
opcGroup.UpdateRate = 1;//刷新频率为1秒。
opcGroup.IsSubscribed = true;//使用订阅功能,即可以异步,默认false
opcGroup1.UpdateRate = 1;//刷新频率为1秒。
opcGroup1.IsSubscribed = true;//使用订阅功能,即可以异步,默认false
//设置组的读写事件
opcGroup.DataChange += OpcGroup_DataChange;
opcGroup.AsyncWriteComplete += OpcGroup_AsyncWriteComplete;
opcGroup.AsyncReadComplete += OpcGroup_AsyncReadComplete;
opcGroup1.DataChange += OpcGroup1_DataChange;
opcGroup1.AsyncWriteComplete += OpcGroup1_AsyncWriteComplete;
opcGroup1.AsyncReadComplete += OpcGroup1_AsyncReadComplete;
//组对象添加项目
AddGroupItems();
}
catch(Exception ex)
{
MessageBox.Show("创建组出错:" + ex.Message);
return false;
}
return true;
}
private void OpcGroup1_AsyncReadComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps, ref Array Errors)
{
for(int i=1;i<=NumItems;i++)
{
object value = ItemValues.GetValue(i);
int clientHandle = (int)ClientHandles.GetValue(i);
for(int j=0;j<opcVariableList.Count;j++)
{
if(clientHandle==j+1)
{
opcVariableList[j].OPCValue = value.ToString();
opcVariableList[j].Quality = Qualities.GetValue(i).ToString();
opcVariableList[j].UpdateTime = TimeStamps.GetValue(i).ToString();
}
}
}
//dgvShow绑定数据源
this.dgvShow.DataSource = null;
this.dgvShow.DataSource = this.opcVariableList;
}
private void OpcGroup1_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors)
{
}
private void OpcGroup1_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
{
}
/// <summary>
/// //组对象添加项目
/// </summary>
private void AddGroupItems()
{
//组对象的项目集合
opcItems = opcGroup.OPCItems;
//opcItems.AddItems()//多个变量一起添加
//项目集合中的项目
opcItem[0] = opcItems.AddItem("ModBus.RTU.age", 0);//byte
opcItem[1] = opcItems.AddItem("ModBus.RTU.height", 1);//short
opcItem[2] = opcItems.AddItem("ModBus.RTU.sex", 2);//bool
opcItem[3] = opcItems.AddItem("ModBus.RTU.words", 3);//string
//组内项目句柄:由于Array和数组的索引值差1,所以数组的第一个元素用0填充占位
int[] temp0 = new int[] { 0, opcItem[0].ServerHandle };
int[] temp1 = new int[] { 0, opcItem[1].ServerHandle };
int[] temp2 = new int[] { 0, opcItem[2].ServerHandle };
int[] temp3 = new int[] { 0, opcItem[3].ServerHandle };
//移除组内项目
Array serverHandle = temp3.ToArray();
Array errors;
opcItems.Remove(1, ref serverHandle, out errors);
//重新添加删除的项目
opcItem[3] = opcItems.AddItem("ModBus.RTU.words", 3);//string
}
/// <summary>
/// 异步读取完成
/// </summary>
/// <param name="TransactionID"></param>
/// <param name="NumItems"></param>
/// <param name="ClientHandles"></param>
/// <param name="ItemValues"></param>
/// <param name="Qualities"></param>
/// <param name="TimeStamps"></param>
/// <param name="Errors"></param>
private void OpcGroup_AsyncReadComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps, ref Array Errors)
{
object[] valueList = new object[4];
//异步读取得到的值顺序是随机的,所以需要根据ClientHandles来对应具体的对象
for(int i=1;i<=4;i++)
{
int index = Convert.ToInt32(ClientHandles.GetValue(i));//获取到的对象的句柄
for(int j=1;j<=4;j++)
{
if(index==j)
{
valueList[j-1] = ItemValues.GetValue(i);
}
}
}
this.labByte.Text = valueList[0].ToString();
this.labShort.Text = valueList[1].ToString();
this.labBool.Text = valueList[2].ToString();
this.labString.Text = valueList[3].ToString();
}
/// <summary>
/// 异步写入完成
/// </summary>
/// <param name="TransactionID"></param>
/// <param name="NumItems"></param>
/// <param name="ClientHandles"></param>
/// <param name="Errors"></param>
private void OpcGroup_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors)
{
MessageBox.Show("数据写入完成!");
}
/// <summary>
/// 订阅读完成
/// </summary>
/// <param name="TransactionID"></param>
/// <param name="NumItems"></param>
/// <param name="ClientHandles"></param>
/// <param name="ItemValues"></param>
/// <param name="Qualities"></param>
/// <param name="TimeStamps"></param>
private void OpcGroup_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
{
//MessageBox.Show("数据读写完成!");
}
#endregion
#region 界面更新定时器事件
/// <summary>
/// 界面更新定时器事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void timerUpate_Tick(object sender, EventArgs e)
{
//OPC服务器的连接状态
if(opcServer.ServerState==(int)OPCServerState.OPCRunning)
{
connectState = true;
this.btnConnectState.BackColor = Color.Green;
}
else
{
connectState = false;
this.btnConnectState.BackColor = Color.Red;
}
if(connectState&&opcVariableList.Count>0)
{
Array errors;
int transActionId = 1;
int cancelId;
opcGroup1.AsyncRead(opcVariableList.Count, serverHandles, out errors, transActionId, out cancelId);
}
}
#endregion
/// <summary>
/// 同步读取事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSynRead_Click(object sender, EventArgs e)
{
if(this.opcServer==null)
{
MessageBox.Show("请先连接服务器!");
return;
}
//同步读取
//object ItemValue;
//object Qualities;
//object TimeStamps;
//opcItem[0].Read(1, out ItemValue, out Qualities, out TimeStamps);//同步读,第一个参数只能为1或2
//this.labByte.Text = (Convert.ToByte(ItemValue)).ToString();
//opcItem[1].Read(1, out ItemValue, out Qualities, out TimeStamps);//同步读,第一个参数只能为1或2
//this.labShort.Text = (Convert.ToInt16(ItemValue)).ToString();
//opcItem[2].Read(1, out ItemValue, out Qualities, out TimeStamps);//同步读,第一个参数只能为1或2
//this.labBool.Text = (Convert.ToBoolean(ItemValue)).ToString();
//opcItem[3].Read(1, out ItemValue, out Qualities, out TimeStamps);//同步读,第一个参数只能为1或2
//this.labString.Text = (Convert.ToString(ItemValue));
//组的同步读取
string[] itemId = { "", "ModBus.RTU.age", "ModBus.RTU.height", "ModBus.RTU.sex", "ModBus.RTU.words" };
int[] clientHandle = { 0, 1, 2, 3, 4 };
Array itemIds = itemId.ToArray();
Array clientHandles = clientHandle.ToArray();
Array serverHandles;
Array errors;
Array values;
object qualities;
object timeStamps;
opcGroup.OPCItems.AddItems(4, ref itemIds, ref clientHandles, out serverHandles, out errors);
opcGroup.SyncRead(2, 4, ref serverHandles, out values, out errors, out qualities, out timeStamps);//第一个参数为2才可以
object[] valueList=new object[4];
for (int i=1;i<=4;i++)
{
valueList[i-1] = values.GetValue(i);
}
this.labByte.Text = valueList[0].ToString();
this.labShort.Text = valueList[1].ToString();
this.labBool.Text = valueList[2].ToString();
this.labString.Text = valueList[3].ToString();
}
/// <summary>
/// 同步写入事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSyncWrite_Click(object sender, EventArgs e)
{
if (this.opcServer == null)
{
MessageBox.Show("请先连接服务器!");
return;
}
//同步写入
//opcItem[3].Write(this.txtString.Text);
//if (this.cbBool.Text == "true")
//{
// opcItem[2].Write(true);
//}
//else
//{
// opcItem[2].Write(false);
//}
//opcItem[1].Write(Convert.ToInt16(this.txtShort.Text));
//opcItem[0].Write(Convert.ToByte(this.txtByte.Text));
//组的同步写入
int[] temp = new int[] { 0, opcItem[0].ServerHandle, opcItem[1].ServerHandle,
opcItem[2].ServerHandle, opcItem[3].ServerHandle };//0用于占位
Array serverHandles = temp.ToArray();
object[] tempValue = new object[] { "", Convert.ToByte(this.txtByte.Text), Convert.ToInt16(this.txtShort.Text),
this.cbBool.Text == "true" ? true : false, this.txtString.Text };//""用于占位
Array values = tempValue.ToArray();
Array errors;
opcGroup.SyncWrite(4, serverHandles, values, out errors);
}
/// <summary>
/// 组的异步写入
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAsyncWrite_Click(object sender, EventArgs e)
{
if (this.opcServer == null)
{
MessageBox.Show("请先连接服务器!");
return;
}
int[] temp = new int[] { 0, opcItem[0].ServerHandle, opcItem[1].ServerHandle,
opcItem[2].ServerHandle, opcItem[3].ServerHandle };//0用于占位
Array serverHandles = temp.ToArray();
object[] tempValue = new object[] { "", Convert.ToByte(this.txtByte.Text), Convert.ToInt16(this.txtShort.Text),
this.cbBool.Text == "true" ? true : false, this.txtString.Text };//""用于占位
Array values = tempValue.ToArray();
Array errors;
int transActionId=1;
int cancelId;
opcGroup.AsyncWrite(4, ref serverHandles, ref values, out errors, transActionId, out cancelId);
}
/// <summary>
/// 异步读取
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAsynRead_Click(object sender, EventArgs e)
{
if (this.opcServer == null)
{
MessageBox.Show("请先连接服务器!");
return;
}
string[] itemId = { "", "ModBus.RTU.age", "ModBus.RTU.height", "ModBus.RTU.sex", "ModBus.RTU.words" };
int[] clientHandle = { 0, 1, 2, 3, 4 };
Array itemIds = itemId.ToArray();
Array clientHandles = clientHandle.ToArray();
Array serverHandles;
Array errors;
int transActionId = 1;
int cancelId;
opcGroup.OPCItems.AddItems(4, ref itemIds, ref clientHandles, out serverHandles, out errors);
opcGroup.AsyncRead(4, serverHandles, out errors, transActionId, out cancelId);
}
private void formMain_FormClosing(object sender, FormClosingEventArgs e)
{
if(opcServer!=null)
{
//释放所有组资源
opcServer.OPCGroups.RemoveAll();
//断开服务器
opcServer.Disconnect();
}
}
/// <summary>
/// 获取OPCServer服务器的变量
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRefreshVariable_Click(object sender, EventArgs e)
{
if(this.opcServer==null)
{
MessageBox.Show("请先连接服务器!");
return;
}
//获取服务器的所有变量
opcBrowser = this.opcServer.CreateBrowser();
opcBrowser.ShowBranches();
opcBrowser.ShowLeafs(true);
this.lvVariableShow.Items.Clear();
//往lvVariableShow中添加变量
foreach(var item in opcBrowser)
{
this.lvVariableShow.Items.Add(item.ToString());
}
}
/// <summary>
/// 双击添加变量
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lvVariableShow_DoubleClick(object sender, EventArgs e)
{
if(this.lvVariableShow.SelectedItems.Count!=0)
{
opcVariableList.Add(new OPCVariable()
{ OPCItemID = this.lvVariableShow.SelectedItems[0].Text });
}
List<string> itemID = new List<string>();
List<int> clientID = new List<int>();
itemID.Add("0");
clientID.Add(0);
for(int i=0;i<opcVariableList.Count;i++)
{
itemID.Add(opcVariableList[i].OPCItemID);
clientID.Add(i + 1);
}
itemIDs = itemID.ToArray();
Array clientIDS = clientID.ToArray();
Array errors;
opcGroup1.OPCItems.AddItems(opcVariableList.Count, ref itemIDs, ref clientIDS, out serverHandles, out errors);
}
/// <summary>
/// 单击变量行头将值显示到编辑控件中
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void dgvShow_RowHeaderMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e)
{
int selectRow=e.RowIndex;
this.txtVarName.Text = this.dgvShow.Rows[selectRow].Cells[0].Value.ToString();
this.txtWriteValue.Text = this.dgvShow.Rows[selectRow].Cells[1].Value.ToString();
}
/// <summary>
/// 将修改后的值写入到服务器中
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnWrite_Click(object sender, EventArgs e)
{
if (this.opcServer == null)
{
MessageBox.Show("请先连接服务器!");
return;
}
OPCItem tempOPCItem = opcItems.AddItem(this.txtVarName.Text, 4);
int[] temp = new int[] { 0, tempOPCItem.ServerHandle };
Array serverHandles = temp.ToArray();
object[] tempValues = new object[2] { 0, Convert.ToInt32(this.txtWriteValue.Text) };
Array values = tempValues.ToArray();
Array errors;
int transActionId = 1;
int cancelId;
opcGroup.AsyncWrite(1, ref serverHandles, ref values, out errors,transActionId,out cancelId);
}
}
}
注意:使用的是32bit的DLL文件,所以只能编译成32bit的执行程序
运行效果: