OPC UA Pub(MQTT代理)模式连接物联网平台

前言

本文介绍如何将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控制台,可以看到设备状态变为“在线”,两个属性也有数值了。

在这里插入图片描述

在看板添加的可视化组件中,也能看到持续更新的数据。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值