C# 读写opc ua服务器,浏览所有节点,读写节点,读历史数据,调用方法,订阅,批量订阅 *** 作

C# 读写opc ua服务器,浏览所有节点,读写节点,读历史数据,调用方法,订阅,批量订阅 *** 作,第1张

概述OPC UA简介 OPC是应用于工业通信的,在windows环境的下一种通讯技术,原有的通信技术难以满足日益复杂的环境,在可扩展性,安全性,跨平台性方面的不足日益明显,所以OPC基金会在几年前提出了面向未来的架构设计的OPC 统一架构,简称OPC UA,截止目前为止,越来越多公司将OPC UA作为开放的数据标准,在未来工业4.0行业上也将大放异彩。   在OPC UA的服务器端。会公开一些数据节点 OPC UA简介

OPC是应用于工业通信的,在windows环境的下一种通讯技术,原有的通信技术难以满足日益复杂的环境,在可扩展性,安全性,跨平台性方面的不足日益明显,所以OPC基金会在几年前提出了面向未来的架构设计的OPC 统一架构,简称OPC UA,截止目前为止,越来越多公司将OPC UA作为开放的数据标准,在未来工业4.0行业上也将大放异彩。

 

在OPC UA的服务器端。会公开一些数据节点,或是方法等信息,允许第三方使用标准的OPC协议来进行访问,在传输层已经安全的处理所有的消息,对于客户端的访问来说,应该是非常清楚简单的。

 

本篇文章是讲述如何开发C#的OPC UA客户端的方式,关于如何开发OPC UA可配置的服务器,请参照另一篇博客:http://www.cnblogs.com/dathlin/p/8976955.html 这篇博客讲述了如何创建基于三菱,西门子,欧姆龙,ModbusTcp客户端,异形ModbusTcp客户端的OPC UA服务器引擎。

 

2.0版本说明

2018年8月18日 20:09:24  基于OPC UA的最新官方库,重新调整了订阅的代码实现,开源地址:https://github.com/dathlin/OpcUaHelper 除了组件的源代码之外,还包含了一个服务器的示例,就是下面的的示例 *** 作。

更加详细的代码说明可以参照GitHub上的readme文件

前期准备

准备好开发的IDE,首选Visual Studio2017版本,新建项目,或是在你原有的项目上进行扩展。注意:项目的.NET Framework版本最低为4.6

打开NuGet管理器,输入指令(如果不明白,参考http://www.cnblogs.com/dathlin/p/7705014.html):

?
1 Install-Package OpcuaHelper

或者:

然后在窗体的界面新增引用:

?
1 using OpcuaHelper;

接下就可以愉快码代码了。

 

 

OPC UA服务器准备

此处有一个供网友测试的服务器:opc.tcp://118.24.36.220:62547/DataAccessServer

当然,一般的网友都会使用Kepware软件,在此处介绍一个我自己开发的OPC UA网关服务器,支持三菱,西门子,欧姆龙,modbustcp客户端转化成OPC UA服务器,支持创建modbus服务器,异形服务器,地址是

https://github.com/dathlin/SharpNodeSettings

 

节点浏览器

我们在得到一个OPC UA的服务器之后,第一件事就是使用节点浏览器对所有的节点进行访问,不然你根本就不知道服务器公开了什么东西,此处我使用了一个测试服务器,该地址为云端地址,不保证以后会不会继续支持访问,目前来说还是可以访问的。

比如这个地址:opc.tcp://118.24.36.220:62547/DataAccessServer

OK,然后我们可以使用代码来显示这个服务器到底有什么数据了!在窗体上新增一个按钮,双击它进入点击事件,写上

?
1 2 3 4 5 6 7 private voID button1_Click( object sender,EventArgs e) {      using (FormbrowseServer form = new FormbrowseServer())      {          form.ShowDialog();      } }

然后就会显示如下的界面:在地址栏输入上述地址,点击连接(此处能连接上的条件是服务器配置为允许匿名登录):

 

左边区域可以随便点击看看,可以看到所有公开的数据,比如点击一个数据节点,下面图片中的name节点,右边编辑框会显示该节点的ID标识,这个标识很重要,关系到等会的读写 *** 作。

客户端实例化

 

?
1 2 3 4 5 6 7 8 9 10 11 private OpcuaClIEnt opcuaClIEnt = new OpcuaClIEnt();   private async voID Form1_Load( object sender,EventArgs e) {      await opcuaClIEnt.ConnectServer( "opc.tcp://118.24.36.220:62547/DataAccessServer" ); }   private voID Form1_FormClosing( object sender,FormClosingEventArgs e) {      opcuaClIEnt.disconnect(); }

如上所示,在窗体载入的时候实例化,在窗体关闭的时候断开连接。下面的节点 *** 作和其他 *** 作使用的实例都是这个opcuaClIEnt,如果你连接的服务器是需要用户名和密码的,那么修改Load中的代码如下:

?
1 2 3 private async voID Form1_Load( object sender,EventArgs e)          {              opcuaClIEnt.UserIDentity = new Opc.Ua.UserIDentity( "admin" , "123456" );
?
1 await opcuaClIEnt.ConnectServer( "opc.tcp://118.24.36.220:62547/DataAccessServer" );
?
1 }

  

节点读取 *** 作

我们要读取一个节点数据,有两个信息是必须知道的

节点的ID标识,就是在上述节点浏览器中的编辑框的信息("ns=2;s=Machines/Machine A/name") 节点的数据类型,这个是必须知道的,不然也不好读取数据。(“string”)

上面的两个信息都可以通过节点浏览器来获取到信息,现在,我们已经获取到了这两个信息,就上面的括号里的数据,然后我们在新增一个按钮,来读取数据:

?
1 2 3 4 5 6 7 8 9 10 11 12 13 private voID button2_Click( object sender,EventArgs e) {      try      {          string value = opcuaClIEnt.ReadNode< string >( "ns=2;s=Machines/Machine A/name" );          MessageBox.Show(value); // 显示测试数据      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }

可以看到,真正的读取数据的 *** 作只有一行代码,但是此处展示了一个良好的编程习惯,使用try..catch..,关于错误捕获的使用以后会专门开篇文章讲解。在展示一个读取float数据类型的示例

?
1 2 3 4 5 6 7 8 9 10 11 12 13 private voID button2_Click( object sender,EventArgs e) {      try      {          float value = opcuaClIEnt.ReadNode< float >( "ns=2;s=Machines/Machine B/TestValuefloat" );          MessageBox.Show(value.ToString()); // 显示100.5      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }

其他的类型参照这种写法就行,哪怕是数组类型也是没有关系的。

类型未知节点读取 *** 作

我们要读取一个节点数据,假设我们只知道一个节点的ID,或者说这个节点的类型是可能变化的,那么我们需要读取到值的同时读取到这个数据的类型,那么代码参照下面

节点的ID标识,就是在上述节点浏览器中的编辑

节点的数据类型最终由 value.WrappedValue.TypeInfo 来决定,有两个属性,是否是数组和基础类型,下面的代码只有int类型进行了严格的数组判断,其他类型参照即可。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40      private voID button3_Click( object sender,EventArgs e)      {          Opc.Ua.DataValue value = opcuaClIEnt.ReadNode( "ns=2;s=Robots/RobotA/RobotMode" );          // 一个数据的类型是不是数组由 value.WrappedValue.TypeInfo.ValueRank 来决定的          // -1 说明是一个数值          // 1  说明是一维数组,如果类型BuiltInType是Int32,那么实际是int[]          // 2  说明是二维数组,如果类型BuiltInType是Int32,那么实际是int[,]          if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.Int32)          {              if (value.WrappedValue.TypeInfo.ValueRank == -1)              {                  int temp = ( int )value.WrappedValue.Value;               // 最终值              }              else if (value.WrappedValue.TypeInfo.ValueRank == 1)              {                  int [] temp = ( int [])value.WrappedValue.Value;           // 最终值              }              else if (value.WrappedValue.TypeInfo.ValueRank == 2)              {                  int [,] temp = ( int [,])value.WrappedValue.Value;         // 最终值              }          }          else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.UInt32)          {              uint temp = ( uint )value.WrappedValue.Value;                 // 数组的情况参照上面的例子          }          else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.float)          {              float temp = ( float )value.WrappedValue.Value;               // 数组的情况参照上面的例子          }          else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.String)          {              string temp = ( string )value.WrappedValue.Value;             // 数组的情况参照上面的例子          }          else if (value.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.DateTime)          {              DateTime temp = (DateTime)value.WrappedValue.Value;         // 数组的情况参照上面的例子          }      } }

 

批量节点读取 *** 作

批量读取节点时,有个麻烦之处在于类型不一定都是一致的,所以为了支持更加广泛的读取 *** 作,只提供Opc.Ua.DataValue的读取,读取到数据后需要自己做一些转换,根据类型来自己转,参照上面类型未知的节点 *** 作代码。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 try {      // 添加所有的读取的节点,此处的示例是类型不一致的情况      List<NodeID> nodeIDs = new List<NodeID>( );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/温度" ) );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/风俗" ) );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/转速" ) );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/机器人关节" ) );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/cvsdf" ) );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/条码" ) );      nodeIDs.Add( new NodeID( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/开关量" ) );        // dataValues按顺序定义的值,每个值里面需要重新判断类型      List<DataValue> dataValues = opcuaClIEnt.ReadNodes( nodeIDs.ToArray() );      // 然后遍历你的数据信息      foreach ( var dataValue in dataValues)      {          // 获取你的实际的数据          object value = dataValue.WrappedValue.Value;      }              // 如果你批量读取的值的类型都是一样的,比如float,那么有简便的方式      List< string > Tags = new List< string >( );      Tags.Add( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/风俗" );      Tags.Add( "ns=2;s=Devices/分厂一/车间二/ModbusTcp客户端/转速" );        // 按照顺序定义的值      List< float > values = opcuaClIEnt.ReadNodes< float >( Tags.ToArray() );   } catch (Exception ex) {      ClIEntUtils.HandleException( this .Text,ex ); }

  

 

 

节点写入 *** 作

 

节点的写入 *** 作和读取类似,我们还是必须要先知道节点的ID和数据类型,和读取最大的区别是,写入的 *** 作很有可能会失败,因为服务器对于数据的输入都是很敏感的,这部分权限肯定会控制的,也就是很有可能会发生写入拒绝,此处的测试服务器允许写入,下面举例在name节点写入“abcd测试写入啊”信息:

?
1 2 3 4 5 6 7 8 9 10 11 12 13 private voID button3_Click( object sender,EventArgs e) {      try      {          bool IsSuccess = opcuaClIEnt.WriteNode( "ns=2;s=Machines/Machine B/name" , "abcd测试写入啊" );          MessageBox.Show(IsSuccess.ToString()); // 显示True,如果成功的话      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }

再写个例子,写入float数据

?
1 2 3 4 5 6 7 8 9 10 11 12 13 private voID button3_Click( object sender,EventArgs e) {      try      {          bool IsSuccess = opcuaClIEnt.WriteNode( "ns=2;s=Machines/Machine B/TestValuefloat" ,123.456f);          MessageBox.Show(IsSuccess.ToString()); // 显示True,如果成功的话      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }

要想查看是否真的写入,可以使用节点数据浏览器来查看是否真的写入。

批量节点写入 *** 作

 写入节点 *** 作时,类型并不一定是统一的,所以此处提供统一的object数组写入,需要注意,对应的节点名称和值的类型必须一致!

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private voID button5_Click( object sender,EventArgs e) {      // 批量写入的代码      string [] nodes = new string []      {          "ns=2;s=Robots/RobotA/RobotMode" ,          "ns=2;s=Robots/RobotA/Userfloat"      };      object [] data = new object []      {          4,          new float []{5,3,1,5,7,8}      };        // 都成功返回True,否则返回False      bool result = opcuaClIEnt.WriteNodes(nodes,data); }

 

数据订阅

下面举例说明订阅ns=2;s=Machines/Machine B/TestValuefloat的数据,我们假设这个在服务器上是不断变化的,按照如下的方式进行数据订阅:

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private voID button2_Click( object sender,EventArgs e ) {      // sub      OpcuaClIEnt.AddSubscription( "A" , "ns=2;s=Machines/Machine B/TestValuefloat" ,SubCallback ); }   private voID SubCallback( string key,MonitoredItem monitoredItem,MonitoredItemNotificationEventArgs args ) {      if (Invokerequired)      {          Invoke( new Action< string ,MonitoredItem,MonitoredItemNotificationEventArgs>( SubCallback ),key,monitoredItem,args );          return ;      }        if (key == "A" )      {          // 如果有多个的订阅值都关联了当前的方法,可以通过key和monitoredItem来区分          MonitoredItemNotification notification = args.NotificationValue as MonitoredItemNotification;          if (notification != null )          {              textBox3.Text = notification.Value.WrappedValue.Value.ToString( );          }      } }

移除订阅

?
1 OpcuaClIEnt.RemoveSubscription( "A" );

  

批量订阅的方式,参照源代码或是 github的说明文件。

 

 

  

方法调用

 

有些OPC 服务器会提供方法调用,测试服务器提供了一个方法,它支持两个int参数输入,string参数输出,方法节点为:ns=2;s=Machines/Machine B/Calculate

我们接下来看看调用服务器的方法到底返回了什么?

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private voID button6_Click( object sender,EventArgs e) {      try      {          string value = opcuaClIEnt.CallMethodByNodeID( "ns=2;s=Machines/Machine B" ,              "ns=2;s=Machines/Machine B/Calculate" ,123,456)[0].ToString();          MessageBox.Show(value); // 显示:我也不知道刚刚发生了什么,调用设备为:Machine B      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }

我们在调用方法的时候需要传入方法的父节点 ID,以及方法的ID,必须先清楚方法的传入参数和传出参数才能对应的代码。

日志输出

OPC UA客户端在运行时会输出一大堆的日志,容量会增加的比较快,是否需要配置,请谨慎处理,如果真的有需要,按照下面的配置方式来完成

?
1 2 3 4 5 private voID button5_Click( object sender,EventArgs e) {      // False 代表每次启动清空日志,True代码不清空,注意,该日志大小增加非常快      opcuaClIEnt.SetLogPathname(Application.StartupPath + "\Logs\opc.ua.clIEnt.txt" , false ); }

上述的都是一些最常用的方法了,已经可以应付大多数的需求,该客户端类还提供了一些连接启动事件,断开事件等等,可以满足额外的需求。

 

引用读取

这种情况比较少,比如服务器端有个MachineB节点,下面放了一些数据,如果客户端把读取的节点写死一般问题也不大,应该服务器很少会改变,但是服务器真的改变了呢。。。。比如在MachineB下追加了一个数据,这种情况确实很少,但是对于我们写成相对动态的情况来说,就很有必要,但是中间问题很多,因为新增的节点类型你是不知道的,ID也是不知道的,所以还先要读取引用,然后在读取数据,然后在判断类型,进行相应的转化。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private voID button6_Click( object sender,EventArgs e) {      try      {          Opc.Ua.ReferenceDescription[] reference = opcuaClIEnt.browseNodeReference( "ns=2;s=Machines/Machine B" );            foreach ( var refer in reference)          {              // 如果不是值节点,就不要了,否则下面读取了也是没有意义的              if (refer.NodeClass != NodeClass.Variable)              {                  continue ;              }                              // 分别读取数据              Opc.Ua.DataValue dataValue = opcuaClIEnt.ReadNode((Opc.Ua.NodeID)refer.NodeID);              if (dataValue.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.Boolean)              {                  // 读取到的是bool数据,在这里做处理                  bool value = ( bool )dataValue.WrappedValue.Value;              }              else if (dataValue.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.String)              {                  // 读取到的是string字符串,在这里做处理                  string value = dataValue.WrappedValue.Value.ToString();              }          }      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }

 

异步 *** 作

 

在读取写入单个节点的功能中,提供了一个异步版本,用来方便的进行异步 *** 作

?
1 2 3 4 5 6 7 8 9 10 11 12 13 private async voID button2_Click( object sender,EventArgs e) {      try      {          float value = await opcuaClIEnt.ReadNodeAsync< float >( "ns=2;s=Machines/Machine B/TestValuefloat" );          MessageBox.Show(value.ToString()); // 显示100.5      }      catch (Exception ex)      {          // 使用了opc ua的错误处理机制来处理错误,网络不通或是读取拒绝          ClIEntUtils.HandleException(Text,ex);      } }
?
1 2 3 4 5 6 7 8 9 10 11 12 13 private async voID button3_Click( object sender,EventArgs e) {      try      {          bool IsSuccess = await opcuaClIEnt.WriteNodeAsync( "ns=2;s=Machines/Machine B/TestValuefloat" ,ex);      } }

  

 

查看本地以注册的服务器

利用官方的控件库来实现的一个 *** 作,允许查看本地的已经注册的服务器。

?
1 2 3 4 5 6 7 8 9 10 11 12 private voID button6_Click( object sender,EventArgs e ) {      // 获取本机已经注册的服务器地址      string endpointUrl = new Opc.Ua.ClIEnt.Controls.discoverServerDlg( ).ShowDialog( opcuaClIEnt.AppConfig, null );      // 获取其他服务器注册的地址,注意,需要该IP的安全策略配置正确      // string endpointUrl = new Opc.Ua.ClIEnt.Controls.discoverServerDlg( ).ShowDialog( opcuaClIEnt.AppConfig,"192.168.0.100" );        if (! string .IsNullOrEmpty( endpointUrl ))      {          // 获取到的需要 *** 作的服务器地址      } }

 

 

触发事件

本opc ua客户端类,包含了几个常用的事件,现在进行说明:

ConnectComplete 事件:在第一次连接到服务器完成的时候触发 ReconnectStarting 事件:开始重新连接到服务器的时候触发 ReconnectComplete 事件:重新连接到服务器的时候触发 KeepAliveComplete 事件:因为opc ua客户端每隔5秒会与服务器进行通讯验证,每次验证都会触发该方法 OpcStatusChange 事件:本OPC UA客户端的终极事件,当客户端的状态变更都会触发,包括了连接,重连,断开,状态激活,opc ua的状态等等

事件类的完整代码如下:

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /// <summary> /// 状态通知的消息类 /// </summary> public class OpcuaStatusEventArgs : EventArgs {      /// <summary>      /// 是否异常      /// </summary>      public bool Error { get ; set ; }      /// <summary>      /// 时间      /// </summary>      public DateTime Time { get ; set ; }      /// <summary>      /// 文本      /// </summary>      public string Text { get ; set ; }        /// <summary>      /// 转化为字符串      /// </summary>      /// <returns></returns>      public overrIDe string ToString()      {          return Error ? "[异常]" : "[正常]" + Time.ToString( "  yyyy-MM-dd HH:mm:ss  " ) + Text;      } }

 

获取客户端网络是否正常有个属性

?
1 2 3 4 5 6 7 /// <summary> /// Indicate the connect status /// </summary> public bool Connected {      get { return m_IsConnected; } }

 

 

特别说明

虽然提供了删除一个节点和新增一个节点的方法,但是在客户端是不允许 *** 作的,调用无效。

总结

以上是内存溢出为你收集整理的C# 读写opc ua服务器,浏览所有节点,读写节点,读历史数据,调用方法,订阅,批量订阅 *** 作全部内容,希望文章能够帮你解决C# 读写opc ua服务器,浏览所有节点,读写节点,读历史数据,调用方法,订阅,批量订阅 *** 作所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1225145.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-05
下一篇 2022-06-05

发表评论

登录后才能评论

评论列表(0条)

保存