在工业树莓派中轻松部署您的OPC UA 服务器与客户端

简介

虹科工业树莓派是一款基于树莓派计算模块的开源的模块化智能网关,其全名是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功能的。但由于得益于其平台的开源性,我们可以自己在RevPi上部署OPC UA Server及Client。虹科Matrikon OPC UA SDK是一款允许您简单迅速地添加一个OPC UA服务器到您嵌入式产品中的软件开发工具包。但由于版权的原因,本文使用开源的open62541进行测试。作为开源项目,open62541相对于虹科Matrikon OPC UA SDK具有不标准、效率低等劣势。作为测试Demo,我们可以不考虑这一点,但在实际现场应用中,建议选择使用更加标准的虹科Matrikon OPC UA SDK进行开发。

1.准备

RevPi Core x1

路由器 x1

网线 x1

open62541源码(可从官网下载)

PC x1

2.编译open62541源码

2.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命令,完成后即可生成以下文件:

2.2 建立Demo

在上一部分,已经通过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

3.OPC UA Server

下面就正式开始编写server程序了,此部分可以参考open62541官方文档。本文的目的不仅仅是建立一个简单的server,还要结合RevPi特有的虚拟Modbus TCP主站的功能,将读取到的Modbus TCP Slave的数据放入OPC UA Server变量中,以实现一个简单的协议转换功能。

3.1 建立Server程序

新建server.c文件,并写入以下代码:

#include “piControlIf.h” #include “piControl.h” #include “open62541.h” #include #include #include #include #include static volatile UA_Boolean running = true; static void stopHandler(int sig) { UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, “received ctrl-c”); running = false; } uint16_t Read_i16u__Val(char *pszVariableName) { int rc; SPIVariable sPiVariable; SPIValue sPIValue; uint16_t i16uValue; strncpy(sPiVariable.strVarName, pszVariableName, sizeof(sPiVariable.strVarName)); rc = piControlGetVariableInfo(&sPiVariable); if (rc < 0) { printf("Cannot find variable '%s' ", pszVariableName); } if (sPiVariable.i16uLength == 16) { rc = piControlRead(sPiVariable.i16uAddress, 4, (uint8_t *) & i16uValue); if (rc < 0) printf("Read error "); else { return i16uValue; } } else printf("Could not read variable %s. Internal Error ", pszVariableName); } static void addVariable(UA_Server *server) { /* Define the attribute of the myInteger variable node */ UA_VariableAttributes attr = UA_VariableAttributes_default; UA_Int16 myInteger = 0; UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT16]); attr.description = UA_LOCALIZEDTEXT("en-US","modbus data"); attr.displayName = UA_LOCALIZEDTEXT("en-US","modbus data"); attr.dataType = UA_TYPES[UA_TYPES_INT16].typeId; attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE; /* Add the variable node to the information model */ UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "modbus.data"); UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, "modbus data"); UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER); UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES); UA_Server_addVariableNode(server, myIntegerNodeId, parentNodeId, parentReferenceNodeId, myIntegerName, UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, NULL); } static void updateModbusData(UA_Server *server) { UA_Int16 new_value = Read_i16u__Val("Input_Word_1"); UA_Variant value; UA_Variant_setScalar(&value, &new_value, &UA_TYPES[UA_TYPES_INT16]); UA_NodeId currentNodeId = UA_NODEID_STRING(1, "modbus.data"); UA_Server_writeValue(server, currentNodeId, value); } static void beforeReadData(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeid, void *nodeContext, const UA_NumericRange *range, const UA_DataValue *data) { updateModbusData(server); } static void addValueCallbackToModbusDataVariable(UA_Server *server) { UA_NodeId currentNodeId = UA_NODEID_STRING(1, "modbus.data"); UA_ValueCallback callback ; callback.onRead = beforeReadData; UA_Server_setVariableNode_valueCallback(server, currentNodeId, callback); } int main() { signal(SIGINT, stopHandler); signal(SIGTERM, stopHandler); UA_Server *server = UA_Server_new(); UA_ServerConfig_setDefault(UA_Server_getConfig(server)); UA_ServerConfig* config = UA_Server_getConfig(server); config->verifyRequestTimestamp = UA_RULEHANDLING_ACCEPT; addVariable(server); addValueCallbackToModbusDataVariable(server); UA_StatusCode retval = UA_Server_run(server, &running); UA_Server_delete(server); return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE; }

3.2 Server程序分析

下面简单分析一下此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/运行。

4.OPC UA Client

4.1 建立Client程序

新建client.c文件,并写入以下代码:

#include “open62541.h” #include #include #include static volatile UA_Boolean reading = true; static void stopHandler(int sig) { UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, “received ctrl-c”); reading = false; } int main(int argc, char *argv[]) { UA_Client *client = UA_Client_new(); UA_ClientConfig_setDefault(UA_Client_getConfig(client)); /* Connect to a server */ UA_StatusCode retval = UA_Client_connect(client, “opc.tcp://localhost:4840″); if(retval != UA_STATUSCODE_GOOD) { UA_Client_delete(client); return EXIT_FAILURE; } /* Read attribute */ UA_Int16 value = 0; while(reading){ signal(SIGINT, stopHandler); signal(SIGTERM, stopHandler); printf(”

Reading the value of node (1, \”modbus.data\”):

“); UA_Variant *val = UA_Variant_new(); retval = UA_Client_readValueAttribute(client, UA_NODEID_STRING(1, “modbus.data”), val); if(retval == UA_STATUSCODE_GOOD && UA_Variant_isScalar(val) && val->type == &UA_TYPES[UA_TYPES_INT16]) { value = *(UA_Int16*)val->data; printf(“the value is: %i

“, value); } UA_Variant_delete(val); sleep(1); } UA_Client_disconnect(client); UA_Client_delete(client); return EXIT_SUCCESS; }

4.2 Client程序分析

相对于Server来说,Client的代码就相对比较简单了。同样是先实例化一个Client并使用默认配置,配置好连接点之后就可以使用UA_Client_readValueAttribute函数读取数据了,在本程序中,我设置了一个循环结构,每一秒读取一次数据。当然OPC UA具有Pub/Sub功能,借助于Pub/Sub功能,我们能够以更具效率的方式获取数据,但此功能在本文中不再进行演示。下面编译Client程序并运行:

gcc client.c open62541.o -o client && ./client

运行效果如下:

5.使用虹科Eurotech网关读取OPC UA数据

上面已经展示了在虹科工业树莓派上运行OPC UA Server以及Client,可以正常运行并读取数据。下面,本文采用其他设备读取运行在虹科工业树莓派上的OPC UA Server的数据,看一下能不能正常运行。在本例中,采用的是虹科Eurotech边缘网关。关于虹科Eurotech边缘网关的详细信息,可以在我们的官网上https://www.hohuln.com/找到,此处不再详细介绍。

首先在ESF的web界面上进行如下配置(注意打开对应的防火墙端口):

然后就可以在Date板块得到获取的数据了。

6.小结

本文采用开源的open62541,在RevPi上部署了OPC UA Server及Client,完成了一个简单的协议转换程序(Modbus TCP转OPC UA),并使用虹科Eurotech网关对数据进行读取,均可正常工作。在实际应用中,您可以根据需要在虹科工业树莓派上灵活配置OPC UA,助您建立自己的IIoT网络。