C#实现与SIMATIC NET OPC DA通讯
2016-02-03 11:35
686 查看
OPC是Object Linking and Embedding(OLE)forProcess Control的缩写,它是微软公司的对象链接和嵌入技术在过程控制方面的应用。OPC以OLE/COM/DCOM技术为基础,采用客户/服务器模式,为工业自动化软件面向对象的开发提供了统一的标准,这个标准定义了应用Microsoft操作系统在基于PC的客户机之间交换自动化实时数据的方法,采用这项标准后,硬件开发商将取代软件开发商为自己的硬件产品开发统一的OPC接口程序,而软件开发者可免除开发驱动程序的工作,充分发挥自己的特长,把更多的精力投入到其核心产品的开发上。
SimaticNet是西门子全集成自动化系统中的一个重要组成部分,它为完善的工业自动化控制系统的通讯提供部件和网络,同时提供多个OPCServer,为数据的外部访问提供接口,本文主要以OPC.SimaticNET为例说明。
90年代OPC基金会开发了一系列的通讯接口比如 Data Access (DA), Alarm & Events (A&E), Historical Data Access (HDA) and Data eXchange (DX),统称传统OPC。今天主要使用的OPC DA通讯方式,这个在1995年左右还是很流行的方法,最近几年OPC Foundation又开发了新的 OPC Unified Architecture (UA) 标准,更好的适应了工业4.0。关于传统OPC和OPC UA的区别,后面会单独来说。
许多OPC服务器,包括OPC.SimaticNet,是在COM平台开发的,从而对于基于.NET框架下的C#语言,作为客户端程序语言访问OPCServer,需要解决两个平台间无缝迁移的问题。OPC基金会对会员提供了OpcRcw动态链接库,OPC NET COM 包装器和OPC NET API,将OPC复杂的规范封状成简单易用的C#类 ,可以比较容易地实现数据访问。
OPC主要包含两种接口:CUSTOM标准接口和OLE自动化标准接口,自定义接口是服务商必须提供的,而自动化接口则是可选的。
自定义接口是一组COM接口,主要用于采用C++语言的应用程序开发;
自动化接口是一组OLE接口,主要用于采用VB,DELPHI,Excel等基于脚本编程语言的应用程序开发。本文是使用C#通过自动化接口来实现的,也是最简单的方式。
首先必须了解的是OPC服务器的对象模型:
程序中涉及到的重要方法和属性比较多,解释下几个容易搞混的:
OPCItem 对象的属性ServerHandle,只读属性,服务器提供给Item的句柄,通过此句柄,Client可以定位到此Item,来对此Item进行后续的操作,比如移动删除;
OPCItem 对象的属性ClientHandle,可读可写属性,客户端分配给Item的句柄,这个句柄可以手动设置,也可由.NET随机选取的,不需要我们来设置,并且每次运行时,这
个句柄都不同,类似于TCP scoket通讯中的Client端分配的端口号。Server端必须指定端口号,Client端随机生成,每次都不一样。
OPCGroup 对象的属性的IsSubscribed,可读可写属性,Group的IsSubscribed为True,此Group才能开始接受服务器的数据属性,此Group才能被订阅。
OPCGroup 对象的事件DataChange (TransactionID As Long, NumItems As Long, ClientHandles() As Long,ItemValues() As Variant, Qualities() As Long, TimeStamps() As Date)需要注意的是NumItems参数是每次事件触发时Group中实际发生数据变化的Item的数量,而不是整个Group里的Items.
OPCGroup 对象的属性UpdateRate,可读可写属性,规定了数据刷新的周期,单位milliseconds.注意的是,不是设定多少ms,实际就是多少,比如给定53ms,OPC server会就近选择50ms.有区间划分的。
源程序如下:
最后,从整体上说下OPC DA的协议规范,OPC DA是在WINDOWS的COM/DOM技术上定义的接口定义,在TCP IP七层模型的最高层应用层,决定了它必须运行在WINDOWS平台,不能够跨平台,灵活性和安全性不如OPC UA,因为OPC DA的会话层和表示层用户是有权利来使用的。对比OPC DA 和OPC UA的协议规范如下:更多不同点,后面会单独再说。
SimaticNet是西门子全集成自动化系统中的一个重要组成部分,它为完善的工业自动化控制系统的通讯提供部件和网络,同时提供多个OPCServer,为数据的外部访问提供接口,本文主要以OPC.SimaticNET为例说明。
90年代OPC基金会开发了一系列的通讯接口比如 Data Access (DA), Alarm & Events (A&E), Historical Data Access (HDA) and Data eXchange (DX),统称传统OPC。今天主要使用的OPC DA通讯方式,这个在1995年左右还是很流行的方法,最近几年OPC Foundation又开发了新的 OPC Unified Architecture (UA) 标准,更好的适应了工业4.0。关于传统OPC和OPC UA的区别,后面会单独来说。
许多OPC服务器,包括OPC.SimaticNet,是在COM平台开发的,从而对于基于.NET框架下的C#语言,作为客户端程序语言访问OPCServer,需要解决两个平台间无缝迁移的问题。OPC基金会对会员提供了OpcRcw动态链接库,OPC NET COM 包装器和OPC NET API,将OPC复杂的规范封状成简单易用的C#类 ,可以比较容易地实现数据访问。
OPC主要包含两种接口:CUSTOM标准接口和OLE自动化标准接口,自定义接口是服务商必须提供的,而自动化接口则是可选的。
自定义接口是一组COM接口,主要用于采用C++语言的应用程序开发;
自动化接口是一组OLE接口,主要用于采用VB,DELPHI,Excel等基于脚本编程语言的应用程序开发。本文是使用C#通过自动化接口来实现的,也是最简单的方式。
首先必须了解的是OPC服务器的对象模型:
程序中涉及到的重要方法和属性比较多,解释下几个容易搞混的:
OPCItem 对象的属性ServerHandle,只读属性,服务器提供给Item的句柄,通过此句柄,Client可以定位到此Item,来对此Item进行后续的操作,比如移动删除;
OPCItem 对象的属性ClientHandle,可读可写属性,客户端分配给Item的句柄,这个句柄可以手动设置,也可由.NET随机选取的,不需要我们来设置,并且每次运行时,这
个句柄都不同,类似于TCP scoket通讯中的Client端分配的端口号。Server端必须指定端口号,Client端随机生成,每次都不一样。
OPCGroup 对象的属性的IsSubscribed,可读可写属性,Group的IsSubscribed为True,此Group才能开始接受服务器的数据属性,此Group才能被订阅。
OPCGroup 对象的事件DataChange (TransactionID As Long, NumItems As Long, ClientHandles() As Long,ItemValues() As Variant, Qualities() As Long, TimeStamps() As Date)需要注意的是NumItems参数是每次事件触发时Group中实际发生数据变化的Item的数量,而不是整个Group里的Items.
OPCGroup 对象的属性UpdateRate,可读可写属性,规定了数据刷新的周期,单位milliseconds.注意的是,不是设定多少ms,实际就是多少,比如给定53ms,OPC server会就近选择50ms.有区间划分的。
源程序如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Net; using OPCAutomation; namespace OpcClient { public partial class OpcClient : Form { public OpcClient() { InitializeComponent(); } #region 私有变量 private String strHostIP; private String strHostName; private Boolean opc_connected; private OPCServer LocalServer; private OPCGroups myGroups; private OPCGroup myGroup; private OPCGroup myGroup1; private OPCItems myItems; private OPCItems myItems1; private OPCItem myItem; int itmHandleClient = 0; /// 客户端句柄 int itmHandleServer = 0; /// 服务端句柄 //**wfx private OPCItem [] myItemArray; private OPCItem[] myItemArray1; #endregion #region 私有方法 /// <summary> /// 连接OPC服务器 /// </summary> /// <param name="remoteServerIP">OPCServerIP</param> /// <param name="remoteServerName">OPCServer名称</param> private bool ConnectRemoteServer(string remoteServerIP, string remoteServerName) { try { LocalServer.Connect(remoteServerName, remoteServerIP); if (LocalServer.ServerState == (int)OPCServerState.OPCRunning) { lblState.Text = "已连接到:" + "\r\n" + LocalServer.ServerName + "\r\n"; //显示服务器信息 lblState.Text = lblState.Text + "开始时间:" + "\r\n" + LocalServer.StartTime.ToString() + "\r\n"; lblState.Text = lblState.Text + "版本:" + LocalServer.MajorVersion.ToString() + "." + LocalServer.MinorVersion.ToString() + "." + LocalServer.BuildNumber.ToString(); } else { //这里你可以根据返回的状态来自定义显示信息,请查看自动化接口API文档 lblState.Text = "状态:" + LocalServer.ServerState.ToString() + "\r\n"; } } catch (Exception err) { MessageBox.Show("连接远程服务器出现错误:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } return true; } /// <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> void myGroup_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps) { //为了测试,所以加了控制台的输出,来查看事物ID号 Console.WriteLine("********"+TransactionID.ToString()+NumItems.ToString()+"*********");//第二次进来后为啥变成1了,之前都是4个 //**wfx /* for (int i = 1; i <= NumItems; i++) { this.txtValue.Text = ItemValues.GetValue(i).ToString(); this.txtQuality.Text = Qualities.GetValue(i).ToString(); this.txtTime.Text = TimeStamps.GetValue(i).ToString(); } */ //**wfx TextBox[] tb = new TextBox[4]; tb[0] = textBox1; tb[1] = textBox2; tb[2] = textBox3; tb[3] = textBox4; //ClientHandles.GetValue(i); for (int i = 1; i <= NumItems;i++ ) { tb[(int)ClientHandles.GetValue(i)-1].Text = ((float)ItemValues.GetValue(i)).ToString("0.00"); } } /// <summary> /// 写入TAG值时执行的事件 /// </summary> /// <param name="TransactionID"></param> /// <param name="NumItems"></param> /// <param name="ClientHandles"></param> /// <param name="Errors"></param> void myGroup_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors) { lblState.Text = ""; for (int i = 1; i <= NumItems; i++) { lblState.Text += "TransactionID:" + TransactionID.ToString() + "\r\n" + "ClientHandle:" + ClientHandles.GetValue(i).ToString() + "\r\n" + "ErrorValue: " + Errors.GetValue(i).ToString() + "\r\n"; } } /// <summary> /// 创建组 /// </summary> private bool CreateGroup() { try { myGroups = LocalServer.OPCGroups; myGroup = myGroups.Add("OpcDotNetGroup"); myGroup1 = myGroups.Add("OpcDotNetGroup1"); SetDefaultGroupProperty(); myItems = myGroup.OPCItems; myItems1 = myGroup1.OPCItems; //**wfx myItemArray =new OPCItem [16]; myItemArray1 = new OPCItem[16]; //**add items //DA格式 S7:[S7 connection_1]MX3.1 //S7:[S7 connection_1]DB1,X3.0 //OPC UA格式 S7:S7 connection_1.db5.68,r //S7:S7 connection_1.db1.20,x7 myItemArray[0] = myItems.AddItem("S7:[S7 connection_1]db5,real68", 1);//Z轴 myItemArray[1] = myItems.AddItem("S7:[S7 connection_1]db1,real50", 2);//刮刀 myItemArray[2] = myItems.AddItem("S7:[S7 connection_1]db1,real96", 3);//送粉 myItemArray[3] = myItems.AddItem("S7:[S7 connection_1]db1,real72", 4);//温度 //**wfx /* myItemArray1[4] = myItems1.AddItem("S7:[S7 connection_1]MX3.1", 4);//Z 正 myItemArray1[5] = myItems1.AddItem("S7:[S7 connection_1]MX3.6", 5);//Z 负 */ myGroup.DataChange += new DIOPCGroupEvent_DataChangeEventHandler(myGroup_DataChange); myGroup.AsyncWriteComplete += new DIOPCGroupEvent_AsyncWriteCompleteEventHandler(myGroup_AsyncWriteComplete); } catch (Exception err) { MessageBox.Show("创建组出现错误:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } return true; } /// <summary> /// 设置缺省的组属性 /// </summary> private void SetDefaultGroupProperty() { LocalServer.OPCGroups.DefaultGroupIsActive = true ; LocalServer.OPCGroups.DefaultGroupDeadband = 0 ; myGroup.UpdateRate = 250 ; myGroup.IsActive = true ; myGroup.IsSubscribed = true ; myGroup1.UpdateRate = 250; myGroup1.IsActive = true; myGroup1.IsSubscribed = true; } #endregion #region 窗体事件 //窗体载入时,查询本机安装的OPC Server 列表 private void OpcClient_Load(object sender, EventArgs e) { //获取本地计算机IP,计算机名称 IPHostEntry IPHost = Dns.GetHostEntry(Environment.MachineName); IPHost.HostName.ToString(); if (IPHost.AddressList.Length > 0) { strHostIP = IPHost.AddressList[0].ToString(); } else { return; } //通过IP来获取计算机名称,可用在局域网内 IPHostEntry ipHostEntry = Dns.GetHostEntry(strHostIP); strHostName = ipHostEntry.HostName.ToString(); //获取本地计算机上的OPCServerName try { LocalServer = new OPCServer(); object serverList = LocalServer.GetOPCServers(strHostName); foreach (string turn in (Array)serverList) { cmbServer.Items.Add(turn); } cmbServer.SelectedIndex = 0; btnConnect.Enabled = true; } catch (Exception err) { MessageBox.Show("枚举本地OPC服务器出错:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } //点击"连接"按钮, 动作 private void btnConnect_Click(object sender, EventArgs e) { //连接服务器 try { if (!ConnectRemoteServer("127.0.0.1", cmbServer.Text)) { return; } opc_connected = true; //已连接标记 OPCBrowser oPCBrowser = LocalServer.CreateBrowser(); //展开分支 oPCBrowser.ShowBranches(); //展开叶子 oPCBrowser.ShowLeafs(true); lstItems.Items.Clear(); //清空列表 foreach (object turn in oPCBrowser) { lstItems.Items.Add(turn.ToString()); } if (!CreateGroup()) { return; } } catch (Exception err) { MessageBox.Show("初始化出错:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } /// 选择列表中的某个Item时处理的事情 private void lstItems_SelectedIndexChanged(object sender, EventArgs e) { try { this.txtName.Text = lstItems.SelectedItem.ToString(); if (itmHandleClient != 0) { this.txtValue.Text = ""; this.txtQuality.Text = ""; this.txtTime.Text = ""; Array Errors; OPCItem bItem = myItems.GetOPCItem(itmHandleServer); //注:OPC中以1为数组的基数 int[] temp = new int[2] { 0, bItem.ServerHandle }; Array serverHandle = (Array)temp; //移除上一次选择的项 myItems.Remove(myItems.Count, ref serverHandle, out Errors); } itmHandleClient = 1234; myItem = myItems.AddItem(lstItems.SelectedItem.ToString(), itmHandleClient); itmHandleServer = myItem.ServerHandle; Console.WriteLine("*****" + itmHandleServer+"*****");//ServerHandle是随机分配,好比Socket的客户端端口号也是随机分配一样,ClientHandle是用户指定, } catch (Exception err) { //没有任何权限的项,都是OPC服务器保留的系统项,此处可不做处理。 itmHandleClient = 0; txtValue.Text = "Error ox"; txtQuality.Text = "Error ox"; txtTime.Text = "Error ox"; MessageBox.Show("此项为系统保留项:" + err.Message, "提示信息"); } } //点击"写入"按钮 private void cmdWrite_Click(object sender, EventArgs e) { OPCItem bItem = myItems.GetOPCItem(itmHandleServer); int[] temp = new int[2] { 0, bItem.ServerHandle }; Array serverHandles = (Array)temp; object[] valueTemp = new object[2] { "", txtNewValue.Text }; Array values = (Array)valueTemp; Array Errors; int cancelID; myGroup.AsyncWrite(1, ref serverHandles, ref values, out Errors, 2009, out cancelID); //myItem.Write(txtNewValue.Text);//这句也可以写入,但并不触发写入事件 GC.Collect(); } //点击"断开" 按钮 private void btnDisConn_Click(object sender, EventArgs e) { if (!opc_connected) { return; } if (myGroup != null) { myGroup.DataChange -= new DIOPCGroupEvent_DataChangeEventHandler(myGroup_DataChange); } if (LocalServer != null) { LocalServer.Disconnect(); //LocalServer = null; } opc_connected = false; lstItems.Items.Clear(); //显示信息 lblState.Text = "已经从OPC服务器断开." + "\r\n" ; //显示服务器信息 lblState.Text = lblState.Text + "断开时间:" + "\r\n" + System.DateTime.Now.ToString() + "\r\n"; } //关闭窗体时候,清理工作 private void OpcClient_FormClosing(object sender, FormClosingEventArgs e) { btnDisConn_Click(new Object(),new EventArgs()); } #endregion private void button1_Click(object sender, EventArgs e) { myItemArray1[6] = myItems1.AddItem("S7:[S7 connection_1]db1,x0.3", 6);//刮刀正 myItemArray1[10] = myItems1.AddItem("S7:[S7 connection_1]db1,real41", 10);//刮刀速度 myItemArray1[11] = myItems1.AddItem("S7:[S7 connection_1]db1,real33", 11);//刮刀位置 int[] temp = new int[4] { 0, myItemArray1[6].ServerHandle, myItemArray1[10].ServerHandle, myItemArray1[11].ServerHandle }; Array serverHandles = (Array)temp; object[] valueTemp = new object[4] { "", true, textBox6.Text, textBox7.Text }; Array values = (Array)valueTemp; Array Errors; int cancelID; myGroup1.AsyncWrite(3, ref serverHandles, ref values, out Errors, 2009, out cancelID); //myItem.Write(txtNewValue.Text);//这句也可以写入,但并不触发写入事件 } private void button2_Click(object sender, EventArgs e) { myItemArray1[7] = myItems1.AddItem("S7:[S7 connection_1]db1,x0.7", 7);//刮刀负 myItemArray1[10] = myItems1.AddItem("S7:[S7 connection_1]db1,real41", 10);//刮刀速度 myItemArray1[11] = myItems1.AddItem("S7:[S7 connection_1]db1,real33", 11);//刮刀位置 int[] temp = new int[4] { 0, myItemArray1[7].ServerHandle, myItemArray1[10].ServerHandle, myItemArray1[11].ServerHandle }; Array serverHandles = (Array)temp; object[] valueTemp = new object[4] { "", true, textBox6.Text, textBox7.Text }; Array values = (Array)valueTemp; Array Errors; int cancelID; myGroup1.AsyncWrite(3, ref serverHandles, ref values, out Errors, 2009, out cancelID); //myItem.Write(txtNewValue.Text);//这句也可以写入,但并不触发写入事件 } private void button3_Click(object sender, EventArgs e) { int[] temp = new int[4] { 0, myItemArray[8].ServerHandle, myItemArray[12].ServerHandle, myItemArray[13].ServerHandle }; Array serverHandles = (Array)temp; object[] valueTemp = new object[4] { "", true, textBox8.Text, textBox9.Text }; Array values = (Array)valueTemp; Array Errors; int cancelID; myGroup.AsyncWrite(3, ref serverHandles, ref values, out Errors, 2009, out cancelID); //myItem.Write(txtNewValue.Text);//这句也可以写入,但并不触发写入事件 } private void button4_Click(object sender, EventArgs e) { int[] temp = new int[4] { 0, myItemArray[9].ServerHandle, myItemArray[12].ServerHandle, myItemArray[13].ServerHandle }; Array serverHandles = (Array)temp; object[] valueTemp = new object[4] { "", true, textBox8.Text, textBox9.Text }; Array values = (Array)valueTemp; Array Errors; int cancelID; myGroup.AsyncWrite(3, ref serverHandles, ref values, out Errors, 2009, out cancelID); //myItem.Write(txtNewValue.Text);//这句也可以写入,但并不触发写入事件 } } }
最后,从整体上说下OPC DA的协议规范,OPC DA是在WINDOWS的COM/DOM技术上定义的接口定义,在TCP IP七层模型的最高层应用层,决定了它必须运行在WINDOWS平台,不能够跨平台,灵活性和安全性不如OPC UA,因为OPC DA的会话层和表示层用户是有权利来使用的。对比OPC DA 和OPC UA的协议规范如下:更多不同点,后面会单独再说。
相关文章推荐
- c#之值类型和引用类型
- c#之字符串的不可变性
- C#反射机制总结
- C# PropertyInfo 将一个对象赋值到另一个相同名称的对象
- c# 借助cmd命令解析apk文件信息
- c#之this显示调用构造函数
- C#读书雷达
- C# 清楚Cookies
- C# 反射 设置字段值无效的解决办法
- c#之new操作符
- C# List使用总结
- CLR via C#深解笔记七 - 自动内存管理(垃圾回收)
- CLR via C#深解笔记六 - 泛型
- C#读书
- .Net and C# release history
- C#设计模式——策略模式
- C#测试题若干,都是基础阿
- C#成神之路<6> 数据类型:浮点以及布尔详解
- C#设计模式——外观模式
- C#成神之路<5> 数据类型:整型详解