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
|
或者:
然后在窗体的界面新增引用:
接下就可以愉快码代码了。
?
?
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"
);
|
节点读取操作
我们要读取一个节点数据,有两个信息是必须知道的
- 节点的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)
????
{
????????
????????
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());
????
}
????
catch
(Exception ex)
????
{
????????
????????
ClientUtils.HandleException(Text,ex);
????
}
}
|
其他的类型参照这种写法就行,哪怕是数组类型也是没有关系的。
类型未知节点读取操作
我们要读取一个节点数据,假设我们只知道一个节点的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"
);
????????
????????
????????
????????
????????
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客户端/开关量"
) );
?
????
????
List<DataValue> dataValues = opcUaClient.ReadNodes( nodeIds.ToArray() );
????
????
foreach
(
var
dataValue
in
dataValues)
????
{
????????
????????
object
value = dataValue.WrappedValue.Value;
????
}
?
?
?
?
????
????
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());
????
}
????
catch
(Exception ex)
????
{
????????
????????
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());
????
}
????
catch
(Exception ex)
????
{
????????
????????
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}
????
};
?
????
????
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 )
{
????
????
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"
)
????
{
????????
????????
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);
????
}
????
catch
(Exception ex)
????
{
????????
????????
ClientUtils.HandleException(Text,ex);
????
}
}
|
我们在调用方法的时候需要传入方法的父节点 ID,以及方法的ID,必须先清楚方法的传入参数和传出参数才能对应的代码。
日志输出
OPC UA客户端在运行时会输出一大堆的日志,容量会增加的比较快,是否需要配置,请谨慎处理,如果真的有需要,按照下面的配置方式来完成
?
1
2
3
4
5
|
private
void
button5_Click(
object
sender,EventArgs e)
{
????
????
opcUaClient.SetLogPathName(Application.StartupPath +
"Logsopc.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
value = (
bool
)dataValue.WrappedValue.Value;
????????????
}
????????????
else
if
(dataValue.WrappedValue.TypeInfo.BuiltInType == Opc.Ua.BuiltInType.String)
????????????
{
????????????????
????????????????
string
value = dataValue.WrappedValue.Value.ToString();
????????????
}
????????
}
????
}
????
catch
(Exception ex)
????
{
????????
????????
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());
????
}
????
catch
(Exception ex)
????
{
????????
????????
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
);
????
????
?
????
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; }
}
|
?
?
特别说明
虽然提供了删除一个节点和新增一个节点的方法,但是在客户端是不允许操作的,调用无效。