虹科工业树莓派是一款基于树莓派计算模块的开源的模块化智能网关,其全名是RevolutionPi(简称RevPi)。
RevPi在计算模块的基础上进行了工业级封装,以实现工业环境的适用性。它取消了不稳定的GPIO接口,通过模块化的DIO以及AIO模块进行扩展。另外它还提供有适用于大多数常见现场总线协议以及工业以太网的网关扩展模块,使得RevPi可以快速集成到您的工业网络中。
在软件方面,RevPi基于树莓派的Raspbain系统,并添加了RT(RealTime)补丁, 支持C、python、Node-RED等高级语言编程,并且内置虚拟Modbus RTU/TCP主从站,无需额外扩展网关即可与您的Modbus设备进行连接。
OPC UA是OPC基金会推出的面向工业4.0的接口规范,它具有架构统一、平台独立、安全可靠、可扩展等特点。OPC UA主要解决了语义互操作性问题,在整个OICT融合中扮演了非常重要的角色。
虹科工业树莓派作为一款模块化的边缘智能网关,默认是没有配备OPC UA功能的。但得益于其平台的开源性,通过使用OPC UA软件开发工具包,我们也可以自己在RevPi上部署OPC UA Server及Client。
虹科提供的Matrikon OPC UA SDK就是这样一款可让您简单迅速地部署OPC UA服务器到您的嵌入式产品中的软件开发工具包。但由于版权的原因,本文使用开源的open62541进行测试。
作为开源项目,open62541相对于虹科Matrikon OPC UA SDK具有不标准、效率低等劣势。仅作为测试Demo,我们可以不考虑这一点,但在实际现场应用中,我们还是建议选择使用更加标准的虹科Matrikon OPC UA SDK进行开发。
RevPi Core *1
路由器 *1
网线 *1
open62541源码(可从官网下载)
PC *1
首先我们需要编译open62541源码,生成对应的.c和.h文件,才能很方便地把open62541集成到我们自己的代码中。
✔我下载的源码版本是open62541-1.1.2.zip,首先通过命令解压文件:
unzip open62541-1.1.2.zip
✔然后进入open62541-1.1.2文件夹,创建build目录并进入,输入以下命令调用cmake:
cmake … DUA_ENABLE_AMALGAMATION=ON
在上一部分,已经通过make命令生成了open62541.c和open62541.h文件。接着我们退出open62541-1.1.2,建立新的文件夹Demo,并将open62541.c和open62541.h文件复制到该文件夹。然后我们就可以在此文件夹中调用open62541编写server及client的程序了。
为了方便后续程序编译调试,我们可以先把open62541.c编译一下,运行以下命令:
gcc -c open62541.c -o open62541.o
新建server.c文件,并写入以下代码:
1.#include "piControlIf.h"
2.#include "piControl.h"
3.#include "open62541.h"
4.#include <stdio.h>
5.#include <stdlib.h>
6.#include <string.h>
7.#include <signal.h>
8.#include <stdlib.h>
9
10.static volatile UA_Boolean running = true;
11.static void stopHandler(int sig) {
12.UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"received ctrl-c");
13. running = false;
14.}
15
16.uint16_t Read_i16u__Val(char *pszVariableName)
17.{
18. int rc;
19. SPIVariable sPiVariable;
20. SPIValue sPIValue;
21. uint16_t i16uValue;
22
23.strncpy(sPiVariable.strVarName,pszVariableName, sizeof(sPiVariable.strVarName));
24. rc = piControlGetVariableInfo(&sPiVariable);
25. if (rc < 0) {
26. printf("Cannot find variable '%s'\n", pszVariableName);
27. }
28. if (sPiVariable.i16uLength == 16) {
29. rc = piControlRead(sPiVariable.i16uAddress, 4, (uint8_t *) & i16uValue);
30.if (rc < 0)
31.printf("Read error\n");
32.else
33. {
34. return i16uValue;
35. }
36.} else
37.printf("Could not read variable %s. Internal Error\n", pszVariableName);
38. }
39
40.static void addVariable(UA_Server *server) {
41. /* Define the attribute of the myInteger variable node */
42.UA_VariableAttributesattr=UA_VariableAttributes_default;
43. UA_Int16 myInteger = 0;
44.UA_Variant_setScalar(&attr.value,&myInteger, &UA_TYPES[UA_TYPES_INT16]);
45.attr.description = UA_LOCALIZEDTEXT("en-US","modbus data");
46.attr.displayName=UA_LOCALIZEDTEXT("enUS","modbus data");
47.attr.dataType=UA_TYPES[UA_TYPES_INT16].typeId; 48.attr.accessLevel=UA_ACCESSLEVELMASK_READ| UA_ACCESSLEVELMASK_WRITE;
49
50. /* Add the variable node to the information model */
51.UA_NodeId myIntegerNodeId =UA_NODEID_STRING(1, "modbus.data");
52.UA_QualifiedName myIntegerName=UA_QUALIFIEDNAME(1, "modbus data");
53.UA_NodeId parentNodeId =UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
54.UA_NodeId parentReferenceNodeId=UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); 55.UA_Server_addVariableNode (server,myIntegerNodeId, parentNodeId,
56.parentReferenceNodeId, myIntegerName,
57.UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),attr, NULL, NULL);
58. }
59
60.static void updateModbusData(UA_Server *server) {
61.UA_Int16 new_value= Read_i16u__Val("Input_Word_1"); 62.UA_Variant value;
63.UA_Variant_setScalar(&value,&new_value, &UA_TYPES[UA_TYPES_INT16]);
64.UA_NodeId currentNodeId = UA_NODEID_STRING(1, "modbus.data");
65.UA_Server_writeValue(server, currentNodeId, value);
66. }
67
68.static void beforeReadData(UA_Server *server,
69. const UA_NodeId *sessionId, void *sessionContext,
70. const UA_NodeId *nodeid, void *nodeContext,
71. const UA_NumericRange *range, const UA_DataValue *data) {
72. updateModbusData(server);
73. }
74.
75.static void addValueCallbackToModbusDataVariable (UA_Server *server) {
76.UA_NodeId currentNodeId =UA_NODEID_STRING(1, "modbus.data");
77.UA_ValueCallback callback ;
78.callback.onRead = beforeReadData;
79.UA_Server_setVariableNode_valueCallback (server, currentNodeId, callback);
80. }
81.
82.int main()
83.{
84. signal(SIGINT, stopHandler);
85. signal(SIGTERM, stopHandler);
86.
87. UA_Server *server = UA_Server_new();
88. UA_ServerConfig_setDefault(UA_Server_getConfig (server)); 89.UA_ServerConfig*config=UA_Server_getConfig(server);
90.config->verifyRequestTimestamp= UA_RULEHANDLING_ACCEPT;
91.
92. addVariable(server);
93. addValueCallbackToModbusDataVariable(server);
94.
95. UA_StatusCode retval = UA_Server_run(server, &running); 96.
97. UA_Server_delete(server);
98. return retval==UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
99.}
下面简单分析一下此Server程序。大概可以分为三个部分Server主程序、添加变量、变量回调。
Server主程序主要包含在main函数中,分为以下几个阶段:
✔ 实例化server
✔ 配置server,使用默认配置
✔ 检测到ctrl+c,server停止运行
✔ 删除server
Server的框架搭建好之后需要向server中添加变量,此部分由addVariable函数完成。此函数的作用就是向server中添加一个nodeid为modbus.data的变量。
最后就是将虚拟Modbus TCP Master读取到的数值放入新建的变量中。因为变量的值需要实时更新,这样client读取到的数据才是最新的。但如果重复不断循环写入的话,会造成占用资源过多,所以我们在此处给变量添加一个回调,只有当client读取这个变量的时候才会同步一下数据,以防止资源的不必要浪费。
此部分功能是由函数addValueCallbackToModbusDataVariable、beforeReadData、updateModbusData完成的。
另外,为了读取到Modbus TCP Master的数据,我们需要从过程映像中取出变量的值,此处的变量为Input_Word_1。幸运的是,这部分不需要我们从头开始写代码,我们可以调用RevPi piTest命令的源码来实现。因此,我们需要include头文件piControlIf.h和piControl.h。Read_i16u__Val就是调用此头文件里的函数从过程映像中读取Input_Word_1的数值的。
本文不再进行代码的详细剖析,有兴趣的可以结合open62541的官方文档深入了解。下面我们看一下代码的运行结果,使用下面的命令编译serve程序:
gcc server.c open62541.o piControlIf.c -o server && ./server
运行结果为:
可以看到server程序成功在opc.tcp://RevPi32692:4840/运行。
新建client.c文件,并写入以下代码:
1. #include "open62541.h"
2.
3. #include <stdlib.h>
4. #include <unistd.h>
5. #include <signal.h>
6.
7. static volatile UA_Boolean reading = true;
8. static void stopHandler(int sig) {
9.UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
10. reading = false;
11. }
12.
13. int main(int argc, char *argv[]) {
14. UA_Client *client = UA_Client_new();
15.UA_ClientConfig_setDefault(UA_Client_getConfig (client));
16.
17. /* Connect to a server */
18.UA_StatusCode retval
= UA_Client_connect(client, "opc.tcp://localhost:4840"); 19. if(retval != UA_STATUSCODE_GOOD) {
20. UA_Client_delete(client);
21. return EXIT_FAILURE;
22. }
23.
24. /* Read attribute */
25. UA_Int16 value = 0;
26. while(reading){
27. signal(SIGINT, stopHandler);
28. signal(SIGTERM, stopHandler);
29.printf("\nReading the value of node
(1, \"modbus.data\"):\n");
30.UA_Variant *val = UA_Variant_new();
31.retval=UA_Client_readValueAttribute(client, UA_NODEID_STRING(1, "modbus.data"), val);
32.if(retval==UA_STATUSCODE_GOOD&& UA_Variant_isScalar(val) &&
33.val->type == &UA_TYPES[UA_TYPES_INT16]) {
34.value = *(UA_Int16*)val->data;
35.printf("the value is: %i\n", value);
36.}
37.UA_Variant_delete(val);
38.sleep(1);
39. }
40.
41. UA_Client_disconnect(client);
42. UA_Client_delete(client);
43. return EXIT_SUCCESS;
44. }
相对于Server来说,Client的代码就相对比较简单了。同样是先实例化一个Client并使用默认配置,配置好连接点之后就可以使用UA_Client_readValueAttribute函数读取数据了,在本程序中,我设置了一个循环结构,每一秒读取一次数据。当然OPC UA具有Pub/Sub功能,借助于Pub/Sub功能,我们能够以更具效率的方式获取数据,但此功能在本文中不再进行演示。下面编译Client程序并运行:
gcc client.c open62541.o -o client && ./client
运行效果如下:
上面已经展示了在虹科工业树莓派上运行OPC UA Server以及Client,可以正常运行并读取数据。下面,本文采用其他设备读取运行在虹科工业树莓派上的OPC UA Server的数据,看一下能不能正常运行。
在本例中,采用的是虹科Eurotech边缘网关。关于虹科Eurotech边缘网关的详细信息,可以在我们的官网上
https://www.hohuln.com/找到,此处不再详细介绍。
首先在ESF的web界面上进行如下配置(注意打开对应的防火墙端口):
本文采用开源的open62541,在RevPi上部署了OPC UA Server及Client,完成了一个简单的协议转换程序(Modbus TCP转OPC UA),并使用虹科Eurotech网关对数据进行读取,均可正常工作。
在实际应用中,您可以根据需要在虹科工业树莓派上灵活配置OPC UA,助您建立自己的IIoT网络。