前言
本文介绍如何将OPC UA地址空间结构化数据使用基于MQTT代理的Pub模式发布到物联网平台,实现可视化展示。
一、功能架构
二、实现步骤
1、编译open62541开源库
本文基于open62541开源库实现OPC UA功能。
首先下载open62541源码,本文使用的版本是v1.3.2。
git clone -b v1.3.2 https://github.com/open62541/open62541.git
进入到下载的源码目录,下载子模块,其中包括OPC UA Pub MQTT功能的一些依赖程序。
cd open62541
git submodule update --init
打开CMakeList.txt文件,修改编译选项,将UA_ENABLE_JSON_ENCODING 、UA_ENABLE_PUBSUB、UA_ENABLE_PUBSUB_MQTT设置为ON。
需要注意不要把UA_ENABLE_AMALGAMATION设置为ON。
在其他情况下推荐把该选项设置为ON,因为这样在编译后所有库文件会集成在单个open62541.h/.c文件中,使用起来比较方便。
但由于OPC UA PubSub MQTT功能还在测试阶段,某些方面还不完善。
经测试v1.4.x和v1.3.x版本源码,如果把UA_ENABLE_AMALGAMATION设置为ON,编译会报错。
修改完编译选项,创建编译目录,然后进行编译。
mkdir build
cd build
cmake .. && make
2、创建工程
编译完成后,在其他路径新创建一个工程目录,目录结构如图所示。
在open62541目录下,需要把open62541源码路径下的部分内容拷过来。
cp -r ${源码路径}/arch ${工程路径}/open62541
cp -r ${源码路径}/deps ${工程路径}/open62541
cp -r ${源码路径}/include ${工程路径}/open62541
cp -r ${源码路径}/plugins ${工程路径}/open62541
cp -r ${源码路径}/src ${工程路径}/open62541
cp -r ${源码路径}/build/src_generated ${工程路径}/open62541
cp ${源码路径}/build/bin/libopen62541.a ${工程路径}/open62541
工程目录中的CMakeList.txt文件内容如下:
cmake_minimum_required (VERSION 3.5)
project (pub_mqtt_demo)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/include)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/src_generated)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/plugins)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/plugins/include)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/arch)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/deps)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/open62541/src/pubsub)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
find_library(OPEN62541_LIB libopen62541.a HINTS ${CMAKE_CURRENT_SOURCE_DIR}/open62541/)
add_executable(mqtt_pub ${CMAKE_CURRENT_SOURCE_DIR}/src/pubsub_mqtt_publish.c)
target_link_libraries(mqtt_pub ${OPEN62541_LIB} pthread)
工程目录src中的pubsub_mqtt_publish.c是OPC UA发布数据到IOT云平台的程序。主体内容是仿照open62541源码中examples/pubsub给的示例程序。
完整程序如下:
#include <open62541/plugin/log_stdout.h>
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <open62541/plugin/pubsub_mqtt.h>
#include "ua_pubsub.h"
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define CONNECTION_NAME "MQTT Publisher Connection"
#define TRANSPORT_PROFILE_URI "http://opcfoundation.org/UA-Profile/Transport/pubsub-mqtt"
#define MQTT_CLIENT_ID "e47aa3ab9f0648dea6af14f49b74b6c6"
#define CONNECTIONOPTION_NAME "mqttClientId"
#define PUBLISHER_TOPIC "data/opcuapub"
#define PUBLISHER_METADATAQUEUENAME "MetaDataTopic"
#define PUBLISHER_METADATAUPDATETIME 0
#define BROKER_ADDRESS_URL "opc.mqtt://bj-2-mqtt.iot-api.com:1883"
//#define BROKER_ADDRESS_URL "opc.mqtt://broker-cn.emqx.io:1883"
//#define BROKER_ADDRESS_URL "opc.mqtt://127.0.0.1:1883"
#define PUBLISH_INTERVAL 5000
// Uncomment the following line to enable MQTT login for the example
#define EXAMPLE_USE_MQTT_LOGIN
#ifdef EXAMPLE_USE_MQTT_LOGIN
#define USERNAME_OPTION_NAME "mqttUsername"
#define PASSWORD_OPTION_NAME "mqttPassword"
#define MQTT_USERNAME "xxxxx"
#define MQTT_PASSWORD "xxx"
#endif
// Uncomment the following line to enable MQTT via TLS for the example
//#define EXAMPLE_USE_MQTT_TLS
#ifdef EXAMPLE_USE_MQTT_TLS
#define USE_TLS_OPTION_NAME "mqttUseTLS"
#define MQTT_CA_FILE_PATH_OPTION_NAME "mqttCaFilePath"
#define CA_FILE_PATH "/path/to/server.cert"
#endif
#define UA_ENABLE_JSON_ENCODING
#ifdef UA_ENABLE_JSON_ENCODING
static UA_Boolean useJson = true;
#else
static UA_Boolean useJson = false;
#endif
static UA_NodeId connectionIdent;
static UA_NodeId publishedDataSetIdent;
static UA_NodeId writerGroupIdent;
static void
addPubSubConnection(UA_Server *server, char *addressUrl) {
/* Details about the connection configuration and handling are located
* in the pubsub connection tutorial */
UA_PubSubConnectionConfig connectionConfig;
memset(&connectionConfig, 0, sizeof(connectionConfig));
connectionConfig.name = UA_STRING(CONNECTION_NAME);
connectionConfig.transportProfileUri = UA_STRING(TRANSPORT_PROFILE_URI);
connectionConfig.enabled = UA_TRUE;
/* configure address of the mqtt broker (local on default port) */
UA_NetworkAddressUrlDataType networkAddressUrl = {UA_STRING_NULL , UA_STRING(addressUrl)};
UA_Variant_setScalar(&connectionConfig.address, &networkAddressUrl,
&UA_TYPES[UA_TYPES_NETWORKADDRESSURLDATATYPE]);
/* Changed to static publisherId from random generation to identify
* the publisher on Subscriber side */
connectionConfig.publisherId.numeric = 2234;
/* configure options, set mqtt client id */
const int connectionOptionsCount = 1
#ifdef EXAMPLE_USE_MQTT_LOGIN
+ 2
#endif
#ifdef EXAMPLE_USE_MQTT_TLS
+ 2
#endif
;
UA_KeyValuePair connectionOptions[connectionOptionsCount];
size_t connectionOptionIndex = 0;
connectionOptions[connectionOptionIndex].key = UA_QUALIFIEDNAME(0, CONNECTIONOPTION_NAME);
UA_String mqttClientId = UA_STRING(MQTT_CLIENT_ID);
UA_Variant_setScalar(&connectionOptions[connectionOptionIndex++].value, &mqttClientId, &UA_TYPES[UA_TYPES_STRING]);
#ifdef EXAMPLE_USE_MQTT_LOGIN
connectionOptions[connectionOptionIndex].key = UA_QUALIFIEDNAME(0, USERNAME_OPTION_NAME);
UA_String mqttUsername = UA_STRING(MQTT_USERNAME);
UA_Variant_setScalar(&connectionOptions[connectionOptionIndex++].value, &mqttUsername, &UA_TYPES[UA_TYPES_STRING]);
connectionOptions[connectionOptionIndex].key = UA_QUALIFIEDNAME(0, PASSWORD_OPTION_NAME);
UA_String mqttPassword = UA_STRING(MQTT_PASSWORD);
UA_Variant_setScalar(&connectionOptions[connectionOptionIndex++].value, &mqttPassword, &UA_TYPES[UA_TYPES_STRING]);
#endif
#ifdef EXAMPLE_USE_MQTT_TLS
connectionOptions[connectionOptionIndex].key = UA_QUALIFIEDNAME(0, USE_TLS_OPTION_NAME);
UA_Boolean mqttUseTLS = true;
UA_Variant_setScalar(&connectionOptions[connectionOptionIndex++].value, &mqttUseTLS, &UA_TYPES[UA_TYPES_BOOLEAN]);
connectionOptions[connectionOptionIndex].key = UA_QUALIFIEDNAME(0, MQTT_CA_FILE_PATH_OPTION_NAME);
UA_String mqttCaFile = UA_STRING(CA_FILE_PATH);
UA_Variant_setScalar(&connectionOptions[connectionOptionIndex++].value, &mqttCaFile, &UA_TYPES[UA_TYPES_STRING]);
#endif
connectionConfig.connectionProperties = connectionOptions;
connectionConfig.connectionPropertiesSize = connectionOptionIndex;
UA_Server_addPubSubConnection(server, &connectionConfig, &connectionIdent);
}
/**
* **PublishedDataSet handling**
* The PublishedDataSet (PDS) and PubSubConnection are the toplevel entities and
* can exist alone. The PDS contains the collection of the published fields. All
* other PubSub elements are directly or indirectly linked with the PDS or
* connection.
*/
static void
addPublishedDataSet(UA_Server *server) {
/* The PublishedDataSetConfig contains all necessary public
* information for the creation of a new PublishedDataSet */
UA_PublishedDataSetConfig publishedDataSetConfig;
memset(&publishedDataSetConfig, 0, sizeof(UA_PublishedDataSetConfig));
publishedDataSetConfig.publishedDataSetType = UA_PUBSUB_DATASET_PUBLISHEDITEMS;
publishedDataSetConfig.name = UA_STRING("Demo PDS");
/* Create new PublishedDataSet based on the PublishedDataSetConfig. */
UA_Server_addPublishedDataSet(server, &publishedDataSetConfig, &publishedDataSetIdent);
}
/**
* **DataSetField handling**
* The DataSetField (DSF) is part of the PDS and describes exactly one published field.
*/
static void
addDataSetField1(UA_Server *server) {
/* Add a field to the previous created PublishedDataSet */
UA_DataSetFieldConfig dataSetFieldConfig;
memset(&dataSetFieldConfig, 0, sizeof(UA_DataSetFieldConfig));
dataSetFieldConfig.dataSetFieldType = UA_PUBSUB_DATASETFIELD_VARIABLE;
dataSetFieldConfig.field.variable.fieldNameAlias = UA_STRING("Temperature");
dataSetFieldConfig.field.variable.promotedField = UA_FALSE;
dataSetFieldConfig.field.variable.publishParameters.publishedVariable =
UA_NODEID_STRING(0, (char*)"Temperature");
dataSetFieldConfig.field.variable.publishParameters.attributeId = UA_ATTRIBUTEID_VALUE;
UA_Server_addDataSetField(server, publishedDataSetIdent, &dataSetFieldConfig, NULL);
}
static void
addDataSetField2(UA_Server *server) {
/* Add a field to the previous created PublishedDataSet */
UA_DataSetFieldConfig dataSetFieldConfig;
memset(&dataSetFieldConfig, 0, sizeof(UA_DataSetFieldConfig));
dataSetFieldConfig.dataSetFieldType = UA_PUBSUB_DATASETFIELD_VARIABLE;
dataSetFieldConfig.field.variable.fieldNameAlias = UA_STRING("Humidity");
dataSetFieldConfig.field.variable.promotedField = UA_FALSE;
dataSetFieldConfig.field.variable.publishParameters.publishedVariable =
UA_NODEID_STRING(0, (char*)"Humidity");
dataSetFieldConfig.field.variable.publishParameters.attributeId = UA_ATTRIBUTEID_VALUE;
UA_Server_addDataSetField(server, publishedDataSetIdent, &dataSetFieldConfig, NULL);
}
/**
* **WriterGroup handling**
* The WriterGroup (WG) is part of the connection and contains the primary configuration
* parameters for the message creation.
*/
static UA_StatusCode
addWriterGroup(UA_Server *server, char *topic, int interval) {
UA_StatusCode retval = UA_STATUSCODE_GOOD;
/* Now we create a new WriterGroupConfig and add the group to the existing PubSubConnection. */
UA_WriterGroupConfig writerGroupConfig;
memset(&writerGroupConfig, 0, sizeof(UA_WriterGroupConfig));
writerGroupConfig.name = UA_STRING("Demo WriterGroup");
writerGroupConfig.publishingInterval = interval;
writerGroupConfig.enabled = UA_FALSE;
writerGroupConfig.writerGroupId = 100;
UA_UadpWriterGroupMessageDataType *writerGroupMessage;
/* decide whether to use JSON or UADP encoding*/
#ifdef UA_ENABLE_JSON_ENCODING
UA_JsonWriterGroupMessageDataType *Json_writerGroupMessage;
if(useJson) {
writerGroupConfig.encodingMimeType = UA_PUBSUB_ENCODING_JSON;
writerGroupConfig.messageSettings.encoding = UA_EXTENSIONOBJECT_DECODED;
writerGroupConfig.messageSettings.content.decoded.type = &UA_TYPES[UA_TYPES_JSONWRITERGROUPMESSAGEDATATYPE];
/* The configuration flags for the messages are encapsulated inside the
* message- and transport settings extension objects. These extension
* objects are defined by the standard. e.g.
* UadpWriterGroupMessageDataType */
Json_writerGroupMessage = UA_JsonWriterGroupMessageDataType_new();
/* Change message settings of writerGroup to send PublisherId,
* DataSetMessageHeader, SingleDataSetMessage and DataSetClassId in PayloadHeader
* of NetworkMessage */
Json_writerGroupMessage->networkMessageContentMask =
(UA_JsonNetworkMessageContentMask)(UA_JSONNETWORKMESSAGECONTENTMASK_NETWORKMESSAGEHEADER |
(UA_JsonNetworkMessageContentMask)UA_JSONNETWORKMESSAGECONTENTMASK_DATASETMESSAGEHEADER |
(UA_JsonNetworkMessageContentMask)UA_JSONNETWORKMESSAGECONTENTMASK_SINGLEDATASETMESSAGE |
(UA_JsonNetworkMessageContentMask)UA_JSONNETWORKMESSAGECONTENTMASK_PUBLISHERID |
(UA_JsonNetworkMessageContentMask)UA_JSONNETWORKMESSAGECONTENTMASK_DATASETCLASSID);
writerGroupConfig.messageSettings.content.decoded.data = Json_writerGroupMessage;
}
else
#endif
{
writerGroupConfig.encodingMimeType = UA_PUBSUB_ENCODING_UADP;
writerGroupConfig.messageSettings.encoding = UA_EXTENSIONOBJECT_DECODED;
writerGroupConfig.messageSettings.content.decoded.type = &UA_TYPES[UA_TYPES_UADPWRITERGROUPMESSAGEDATATYPE];
/* The configuration flags for the messages are encapsulated inside the
* message- and transport settings extension objects. These extension
* objects are defined by the standard. e.g.
* UadpWriterGroupMessageDataType */
writerGroupMessage = UA_UadpWriterGroupMessageDataType_new();
/* Change message settings of writerGroup to send PublisherId,
* WriterGroupId in GroupHeader and DataSetWriterId in PayloadHeader
* of NetworkMessage */
writerGroupMessage->networkMessageContentMask =
(UA_UadpNetworkMessageContentMask)(UA_UADPNETWORKMESSAGECONTENTMASK_PUBLISHERID |
(UA_UadpNetworkMessageContentMask)UA_UADPNETWORKMESSAGECONTENTMASK_GROUPHEADER |
(UA_UadpNetworkMessageContentMask)UA_UADPNETWORKMESSAGECONTENTMASK_WRITERGROUPID |
(UA_UadpNetworkMessageContentMask)UA_UADPNETWORKMESSAGECONTENTMASK_PAYLOADHEADER);
writerGroupConfig.messageSettings.content.decoded.data = writerGroupMessage;
}
/* configure the mqtt publish topic */
UA_BrokerWriterGroupTransportDataType brokerTransportSettings;
memset(&brokerTransportSettings, 0, sizeof(UA_BrokerWriterGroupTransportDataType));
/* Assign the Topic at which MQTT publish should happen */
/*ToDo: Pass the topic as argument from the writer group */
brokerTransportSettings.queueName = UA_STRING(topic);
brokerTransportSettings.resourceUri = UA_STRING_NULL;
brokerTransportSettings.authenticationProfileUri = UA_STRING_NULL;
/* Choose the QOS Level for MQTT */
brokerTransportSettings.requestedDeliveryGuarantee = UA_BROKERTRANSPORTQUALITYOFSERVICE_BESTEFFORT;
/* Encapsulate config in transportSettings */
UA_ExtensionObject transportSettings;
memset(&transportSettings, 0, sizeof(UA_ExtensionObject));
transportSettings.encoding = UA_EXTENSIONOBJECT_DECODED;
transportSettings.content.decoded.type = &UA_TYPES[UA_TYPES_BROKERWRITERGROUPTRANSPORTDATATYPE];
transportSettings.content.decoded.data = &brokerTransportSettings;
writerGroupConfig.transportSettings = transportSettings;
retval = UA_Server_addWriterGroup(server, connectionIdent, &writerGroupConfig, &writerGroupIdent);
if (retval == UA_STATUSCODE_GOOD)
UA_Server_setWriterGroupOperational(server, writerGroupIdent);
#ifdef UA_ENABLE_JSON_ENCODING
if (useJson) {
UA_JsonWriterGroupMessageDataType_delete(Json_writerGroupMessage);
}
#endif
if (!useJson && writerGroupMessage) {
UA_UadpWriterGroupMessageDataType_delete(writerGroupMessage);
}
return retval;
}
/**
* **DataSetWriter handling**
* A DataSetWriter (DSW) is the glue between the WG and the PDS. The DSW is
* linked to exactly one PDS and contains additional information for the
* message generation.
*/
static void
addDataSetWriter(UA_Server *server, char *topic) {
/* We need now a DataSetWriter within the WriterGroup. This means we must
* create a new DataSetWriterConfig and add call the addWriterGroup function. */
UA_NodeId dataSetWriterIdent;
UA_DataSetWriterConfig dataSetWriterConfig;
memset(&dataSetWriterConfig, 0, sizeof(UA_DataSetWriterConfig));
dataSetWriterConfig.name = UA_STRING("Demo DataSetWriter");
dataSetWriterConfig.dataSetWriterId = 62541;
dataSetWriterConfig.keyFrameCount = 10;
#ifdef UA_ENABLE_JSON_ENCODING
UA_JsonDataSetWriterMessageDataType jsonDswMd;
UA_ExtensionObject messageSettings;
if(useJson) {
/* JSON config for the dataSetWriter */
jsonDswMd.dataSetMessageContentMask = (UA_JsonDataSetMessageContentMask)
(UA_JSONDATASETMESSAGECONTENTMASK_DATASETWRITERID |
UA_JSONDATASETMESSAGECONTENTMASK_SEQUENCENUMBER |
UA_JSONDATASETMESSAGECONTENTMASK_STATUS |
UA_JSONDATASETMESSAGECONTENTMASK_METADATAVERSION |
UA_JSONDATASETMESSAGECONTENTMASK_TIMESTAMP);
messageSettings.encoding = UA_EXTENSIONOBJECT_DECODED;
messageSettings.content.decoded.type = &UA_TYPES[UA_TYPES_JSONDATASETWRITERMESSAGEDATATYPE];
messageSettings.content.decoded.data = &jsonDswMd;
dataSetWriterConfig.messageSettings = messageSettings;
}
#endif
/*TODO: Modify MQTT send to add DataSetWriters broker transport settings */
/*TODO: Pass the topic as argument from the writer group */
/*TODO: Publish Metadata to metaDataQueueName */
/* configure the mqtt publish topic */
UA_BrokerDataSetWriterTransportDataType brokerTransportSettings;
memset(&brokerTransportSettings, 0, sizeof(UA_BrokerDataSetWriterTransportDataType));
/* Assign the Topic at which MQTT publish should happen */
brokerTransportSettings.queueName = UA_STRING(topic);
brokerTransportSettings.resourceUri = UA_STRING_NULL;
brokerTransportSettings.authenticationProfileUri = UA_STRING_NULL;
brokerTransportSettings.metaDataQueueName = UA_STRING(PUBLISHER_METADATAQUEUENAME);
brokerTransportSettings.metaDataUpdateTime = PUBLISHER_METADATAUPDATETIME;
/* Choose the QOS Level for MQTT */
brokerTransportSettings.requestedDeliveryGuarantee = UA_BROKERTRANSPORTQUALITYOFSERVICE_BESTEFFORT;
/* Encapsulate config in transportSettings */
UA_ExtensionObject transportSettings;
memset(&transportSettings, 0, sizeof(UA_ExtensionObject));
transportSettings.encoding = UA_EXTENSIONOBJECT_DECODED;
transportSettings.content.decoded.type = &UA_TYPES[UA_TYPES_BROKERDATASETWRITERTRANSPORTDATATYPE];
transportSettings.content.decoded.data = &brokerTransportSettings;
dataSetWriterConfig.transportSettings = transportSettings;
UA_Server_addDataSetWriter(server, writerGroupIdent, publishedDataSetIdent,
&dataSetWriterConfig, &dataSetWriterIdent);
}
typedef struct container {
UA_Server * ptr1;
UA_NodeId * ptr2;
UA_NodeId * ptr3;
} container_t;
void *write_var(void *arg)
{
container_t *pContainer = (container_t*)arg;
UA_Server *server = pContainer->ptr1;
UA_NodeId *targetNodeId_1 = pContainer->ptr2;
UA_NodeId *targetNodeId_2 = pContainer->ptr3;
while (1)
{
sleep(2);
double new_value1 = 30+rand()%(40-30+1);
UA_Variant temperature_value;
UA_Variant_init(&temperature_value);
UA_Variant_setScalar(&temperature_value, &new_value1, &UA_TYPES[UA_TYPES_DOUBLE]);
UA_Server_writeValue(server, *targetNodeId_1, temperature_value);
double new_value2 = 60+rand()%(80-60+1);
UA_Variant humidity_value;
UA_Variant_init(&humidity_value);
UA_Variant_setScalar(&humidity_value, &new_value2, &UA_TYPES[UA_TYPES_DOUBLE]);
UA_Server_writeValue(server, *targetNodeId_2, humidity_value);
}
return NULL;
}
/**
* That's it! You're now publishing the selected fields. Open a packet
* inspection tool of trust e.g. wireshark and take a look on the outgoing
* packages. The following graphic figures out the packages created by this
* tutorial.
*
* .. figure:: ua-wireshark-pubsub.png
* :figwidth: 100 %
* :alt: OPC UA PubSub communication in wireshark
*
* The open62541 subscriber API will be released later. If you want to process
* the the datagrams, take a look on the ua_network_pubsub_networkmessage.c
* which already contains the decoding code for UADP messages.
*
* It follows the main server code, making use of the above definitions. */
UA_Boolean running = true;
static void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
static void usage(void) {
printf("Usage: tutorial_pubsub_mqtt_publish [--url <opc.mqtt://hostname:port>] "
"[--topic <mqttTopic>] "
"[--freq <frequency in ms> "
"[--json]\n"
" Defaults are:\n"
" - Url: opc.mqtt://127.0.0.1:1883\n"
" - Topic: customTopic\n"
" - Frequency: 500\n"
" - JSON: Off\n");
}
int main(int argc, char **argv) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
/* TODO: Change to secure mqtt port:8883 */
char *addressUrl = BROKER_ADDRESS_URL;
char *topic = PUBLISHER_TOPIC;
int interval = PUBLISH_INTERVAL;
/* Parse arguments */
for(int argpos = 1; argpos < argc; argpos++) {
if(strcmp(argv[argpos], "--help") == 0) {
usage();
return 0;
}
if(strcmp(argv[argpos], "--json") == 0) {
useJson = true;
continue;
}
if(strcmp(argv[argpos], "--url") == 0) {
if(argpos + 1 == argc) {
usage();
return -1;
}
argpos++;
addressUrl = argv[argpos];
continue;
}
if(strcmp(argv[argpos], "--topic") == 0) {
if(argpos + 1 == argc) {
usage();
return -1;
}
argpos++;
topic = argv[argpos];
continue;
}
if(strcmp(argv[argpos], "--freq") == 0) {
if(argpos + 1 == argc) {
usage();
return -1;
}
if(sscanf(argv[argpos], "%d", &interval) != 1) {
usage();
return -1;
}
if(interval <= 10) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Publication interval too small");
return -1;
}
continue;
}
usage();
return -1;
}
UA_StatusCode retval = UA_STATUSCODE_GOOD;
/* Set up the server config */
UA_Server *server = UA_Server_new();
UA_ServerConfig *config = UA_Server_getConfig(server);
UA_ServerConfig_setMinimal(config, 4840, NULL);
/* Details about the connection configuration and handling are located in
* the pubsub connection tutorial */
UA_ServerConfig_addPubSubTransportLayer(config, UA_PubSubTransportLayerMQTT());
UA_VariableAttributes attr1 = UA_VariableAttributes_default;
attr1.displayName = UA_LOCALIZEDTEXT("en-US", "Temperature");
attr1.description = UA_LOCALIZEDTEXT("en-US", "Temperature");
attr1.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
attr1.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_NodeId nodeid1 = UA_NODEID_STRING(0, (char*)"Temperature");
UA_Server_addVariableNode(server,
nodeid1,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(0, (char*)"Temperature"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
attr1, NULL, NULL);
UA_VariableAttributes attr2 = UA_VariableAttributes_default;
attr2.displayName = UA_LOCALIZEDTEXT("en-US", "Humidity");
attr2.description = UA_LOCALIZEDTEXT("en-US", "Humidity");
attr2.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
attr2.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
UA_NodeId nodeid2 = UA_NODEID_STRING(0, (char*)"Humidity");
UA_Server_addVariableNode(server,
nodeid2,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(0, (char*)"Humidity"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
attr2, NULL, NULL);
container_t cont = {server, &nodeid1, &nodeid2};
pthread_t tid;
int err = pthread_create(&tid, NULL, write_var, &cont);
if (err != 0)
{
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Create thread Fail.");
return -1;
}
addPubSubConnection(server, addressUrl);
addPublishedDataSet(server);
addDataSetField1(server);
addDataSetField2(server);
retval = addWriterGroup(server, topic, interval);
if (UA_STATUSCODE_GOOD != retval)
{
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Error Name = %s", UA_StatusCode_name(retval));
return EXIT_FAILURE;
}
addDataSetWriter(server, topic);
UA_PubSubConnection *connection = UA_PubSubConnection_findConnectionbyId(server, connectionIdent);
if(!connection) {
UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Could not create a PubSubConnection");
UA_Server_delete(server);
return -1;
}
UA_Server_run(server, &running);
UA_Server_delete(server);
return 0;
}
程序中,在地址空间里创建了两个double类型的变量节点,用来模拟温湿度传感器的温度、湿度数据。还写了一个线程用来向这两个变量写入随机值。
我们知道OPC UA的Pub/Sub功能在程序实现上是基于OPC UA Server的,创建Pub、Sub要先创建Server。同时,发布数据就是发布的Server地址空间中的变量节点数据,从外部订阅的数据也是要写入到Server地址空间的变量节点上。
程序里就是把地址空间里创建的两个变量发布了出去。
需要注意程序开头部分的宏定义,其中以下几个宏定义要根据实际情况修改:
#define PUBLISHER_TOPIC "data/opcuapub" // 发布话题(Topic)
#define BROKER_ADDRESS_URL "opc.mqtt://broker-cn.emqx.io:1883" // MQTT代理服务器地址
#define MQTT_USERNAME "xxxxx" // MQTT用户名
#define MQTT_PASSWORD "xxx" // MQTT密码
在物联网平台进行配置之后,按照平台提供参数修改上述几个宏定义,才能在平台订阅到OPC UA Pub端发布的数据。
但在此之前,我们先使用MQTT代理工具测试程序的效果,观察一下OPC UA Pub端发布数据的格式。
3、测试OPC UA发布数据
使用mosquitto测试,linux系统直接apt安装即可。mosquitto会提供一个本地的MQTT代理,还能使用命令行工具设置MQTT发布端和订阅端。
本文我们先用OPC UA Pub程序向mosquitto本地MQTT代理发布数据,然后用命令行工具开启MQTT订阅端,订阅OPC UA Pub端发布的数据。
(1)修改OPC UA Pub程序开头的宏定义
#define PUBLISHER_TOPIC "data/opcuapub" // 发布话题(Topic)
#define BROKER_ADDRESS_URL "opc.mqtt://127.0.0.1:1883" // MQTT代理服务器地址
#define MQTT_USERNAME "test" // MQTT用户名
#define MQTT_PASSWORD "123123" // MQTT密码
(2)保存后编译工程
进入工程路径下的build目录,然后编译
cd build
cmake .. && make
编译后的可执行程序是mqtt_pub
(3)运行OPC UA Pub端程序
./mqtt_pub
程序运行之后可以看到在持续发布数据。
(4)开启MQTT订阅端
输入以下命令开启MQTT订阅端
mosquitto_sub -h 127.0.0.1 -t data/opcuapub -u test -P 123123
-h:设置MQTT代理服务器的地址,这里用的是本地代理,所以写127.0.0.1
-t: 设置订阅话题,需要和OPC UA Pub程序里的PUBLISHER_TOPIC宏定义一致
-u:设置MQTT用户名,需要和OPC UA Pub程序里的MQTT_USERNAME宏定义一致
-P:设置MQTT密码,需要和OPC UA Pub程序里的MQTT_PASSWORD宏定义一致
红框里就是订阅到的一条数据,数据内容比较复杂,但我们最关心的是OPC UA Pub端发布的两个double类型的变量,即温度数值和湿度数值。可以看到两个变量在Payload里,变量值在Body后面。
{"MessageId":null,"MessageType":"ua-data","Messages":[{"DataSetWriterId":62541,"SequenceNumber":37,"MetaDataVersion":{"MajorVersion":2924549898,"MinorVersion":2924549828},"Timestamp":"2025-01-24T16:13:33.510988Z","Payload":{"Temperature":{"Type":11,"Body":37},"Humidity":{"Type":11,"Body":69}}}]}
在与物联网平台对接前,先观察OPC UA Pub端发布数据的格式是十分必要的。
因为物联网平台一般都定义了发布数据的标准数据格式(比如JSON),而像红框里的数据格式就不符合平台标准,需要先借助平台工具转换为标准格式,之后才能被平台识别。
因此我们得先了解OPC UA Pub端发布数据的实际格式,之后才能在平台中编写格式转换程序。
4、配置ThingsCloud平台
本文以ThingsCloud物联网平台为例,演示如何实现在平台中订阅OPC UA Pub发布的数据,并将数据显示到可视化组件上。
注册ThingsCloud账户后登录,进入控制台后创建项目,然后进入项目后台。
4.1、首先创建设备类型
(1)点击创建设备类型
(2)填写设备类型信息
(3)选择创建的设备类型,进入功能定义,添加属性
(4)填写属性信息
注意属性名称要和OPC UA地址空间中的变量节点名称一致。
4.2 为设备类型添加自定义数据流
(1)选择设备类型,添加自定义数据流
由于OPC UA Pub发布的数据并不符合ThingsCloud标准接入协议的JSON格式,因此需要创建自定义数据流,并在后面对该数据流进行格式转换。
(2)填写自定义数据流信息
默认会使用数据流标识符生成该数据流的Topic名称。
(3)创建后会自动启用自定义数据流
可以看到发布Topic名称是data/opcuapub,因此OPC UA Pub程序PUBLISHER_TOPIC宏定义要改为“data/opcuapub”。
4.3 创建设备
(1)点击创建设备
(2)填写设备信息
设备类型选择刚创建的类型。
创建完成后,可以看到设备接入信息。按照信息修改OPC UA Pub程序BROKER_ADDRESS_URL、MQTT_USERNAME、MQTT_PASSWORD三个宏定义。
4.4 为设备添加数据解析规则
(1)选择设备,创建规则
(2)填写规则信息
规则类型选择“自定义上报”,自定义数据流选择给设备类型添加的数据流标识符
(3)添加解析函数
为规则添加操作,选择属性解析函数。
平台支持使用Javascript语言自定义解析函数,对设备发布的数据流进行解析,转换为平台支持的JSON数据格式。
module.exports = function (identifier, data) {
var attributes = {};
const text = data.toString();
const tmp1 = text.split("\"Payload\":{");
const tmp2 = tmp1[1].split("}}]}");
const tmp3 = tmp2[0].split("},");
tmp3.forEach(tmp3 => {
const parts = tmp3.split(":");
const key = parts[0].split("\"");
const value = parts[3].split("}");
attributes[key[1]] = value[0]*1;
})
return attributes;
}
(4)添加后会自动启用规则
4.5 添加可视化组件
(1)创建看板
(2)填写看板信息
(3)添加组件
(4)选择组件样式
(5)设置数据源
三、实现效果
对OPC UA Pub程序宏定义修改之后,重新编译,然后运行程序。
进入ThingsCloud控制台,可以看到设备状态变为“在线”,两个属性也有数值了。
在看板添加的可视化组件中,也能看到持续更新的数据。