学习open62541 --- [5] Server连接变量和物理过程

在OPC UA Server里,往往会有很多runtime信息,这些信息由底层的某种物理过程产生,如锅炉的温度值,是在锅炉运行过程中产生的,锅炉运行过程就可以看做是一个物理过程。

Server会提供一个变量,这个变量存放锅炉的温度值,这样Client通过读取这个温度值就能知道锅炉的温度了。

本文为了简化,就以系统时间为例,讲述如何把一个变量和系统时间联系在一起,这样client通过该变量就可以获取系统时间,系统时间是不断变化的,可以看做一个物理过程。


一 手动更新变量

我们在OPC UA Server里添加一个变量用来表示系统时间,然后手动更新它,代码如下,

// server.c

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include 
#include 

#include "open62541.h"

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 updateCurrentTime(UA_Server * server, UA_NodeId timeNodeId)
{
	UA_DateTime now = UA_DateTime_now();
	UA_Variant value;
	UA_Variant_setScalar(&value, &now, &UA_TYPES[UA_TYPES_DATETIME]);
	UA_Server_writeValue(server, timeNodeId, value);
}


static void addCurrentTimeVariable(UA_Server * server)
{
	UA_DateTime now = 0;
	UA_VariableAttributes attr = UA_VariableAttributes_default;
	attr.displayName = UA_LOCALIZEDTEXT("en-US", "Current time - value callback");
	attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
	UA_Variant_setScalar(&attr.value, &now, &UA_TYPES[UA_TYPES_DATETIME]);
    
    // 添加变量表示时间
	UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time-value-callback");
    UA_QualifiedName currentName = UA_QUALIFIEDNAME(1, "current-time-value-callback");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_NodeId variableTypeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);
    UA_Server_addVariableNode(server, currentNodeId, parentNodeId,
                              parentReferenceNodeId, currentName,
                              variableTypeNodeId, attr, NULL, NULL);
    
    // 手动更新一次时间
    updateCurrentTime(server, currentNodeId);

}

int main(void) 
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    addCurrentTimeVariable(server);

    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

代码里添加了时间变量后,调用updateCurrentTime()更新了一次时间,编译代码并运行,然后使用UaExpert连接,显示如下,
学习open62541 --- [5] Server连接变量和物理过程_第1张图片
其值如下,
学习open62541 --- [5] Server连接变量和物理过程_第2张图片


二 变量值读写的回调

前面添加了时间变量并手动更新了其值,但是系统时间是时刻变化的,为了保证变量的值和系统时间同步,就需要不断的手动更新,因为clients可能随时都要去读取这个变量来获取系统时间。这就要求servers不断的更新时间,就会消耗很多资源。

所以可以给这个变量添加回调,只有当clients需要读取系统时间时才去做一下同步,这样就省去了无谓的资源消耗。

添加回调的代码如下,

// server.c

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include 
#include 

#include "open62541.h"

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 updateCurrentTime(UA_Server * server, UA_NodeId timeNodeId)
{
	UA_DateTime now = UA_DateTime_now();
	UA_Variant value;
	UA_Variant_setScalar(&value, &now, &UA_TYPES[UA_TYPES_DATETIME]);
	UA_Server_writeValue(server, timeNodeId, value);
}


static void addCurrentTimeVariable(UA_Server * server)
{
	UA_DateTime now = 0;
	UA_VariableAttributes attr = UA_VariableAttributes_default;
	attr.displayName = UA_LOCALIZEDTEXT("en-US", "Current time - value callback");
	attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
	UA_Variant_setScalar(&attr.value, &now, &UA_TYPES[UA_TYPES_DATETIME]);

	UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time-value-callback");
    UA_QualifiedName currentName = UA_QUALIFIEDNAME(1, "current-time-value-callback");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_NodeId variableTypeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);
    UA_Server_addVariableNode(server, currentNodeId, parentNodeId,
                              parentReferenceNodeId, currentName,
                              variableTypeNodeId, attr, NULL, NULL);

    updateCurrentTime(server, currentNodeId );

}


static void beforeReadTime(UA_Server *server,
               const UA_NodeId *sessionId, void *sessionContext,
               const UA_NodeId *nodeid, void *nodeContext,
               const UA_NumericRange *range, const UA_DataValue *data) 
{
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "The variable was to be updated");
    updateCurrentTime(server, *nodeid);
}

static void afterWriteTime(UA_Server *server,
               const UA_NodeId *sessionId, void *sessionContext,
               const UA_NodeId *nodeId, void *nodeContext,
               const UA_NumericRange *range, const UA_DataValue *data) 
{
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "The variable was updated");
}

static void addValueCallbackToCurrentTimeVariable(UA_Server *server) 
{
    UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time-value-callback");
    UA_ValueCallback callback ;
    callback.onRead = beforeReadTime;
    callback.onWrite = afterWriteTime;
    UA_Server_setVariableNode_valueCallback(server, currentNodeId, callback);
}


int main(void) 
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    addCurrentTimeVariable(server);
    addValueCallbackToCurrentTimeVariable(server);

    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

代码解析

  1. 使用UA_ValueCallback创建回调,并添加2个回调函数,一个是读之前beforeReadTime(),另一个是写之后afterWriteTime()
  2. 使用UA_Server_setVariableNode_valueCallback()给时间变量添加第1步创建的回调
  3. 当Client读取时间变量时,Server就会先去调用beforeReadTime(),该函数对时间变量进行更新
  4. 最后Client就可以通过时间变量的值获取Server的当前系统时间

编译运行后,使用UaExpert去连接,显示如下,
学习open62541 --- [5] Server连接变量和物理过程_第3张图片
我们用鼠标单击"Current time - value callback",然后在server端看到打印信息,
在这里插入图片描述
可以看到回调已经执行了,因为点击这个变量,UaExpert就会去读取其值,然后显示出来。
学习open62541 --- [5] Server连接变量和物理过程_第4张图片
当我们鼠标点击别的地方,即取消点击"Current time - value callback",然后再点回来,就会又触发回调,最直观的表现就是时间值变了,
学习open62541 --- [5] Server连接变量和物理过程_第5张图片
然后在server端可以看到又出现了一次打印,
在这里插入图片描述
可能会有疑问:我们只是去读变量的值,怎么会触发写之后的回调呢?
这是因为在读的回调里调用了updateCurrentTime(),这个函数里调用了UA_Server_writeValue(),就是对变量进行写操作,这样就触发了变量的写之后的回调。


三 变量的数据源

前2节的操作中,变量的值都是存放在变量节点里的,client都是从变量里拿到值,也就意味着变量需要先从对应的物理过程中拿到最新值,然后保存下来,最后才能让client读取。

本节就更进一步,直接从源头拿数据,不用去读取变量本身的值,这就需要我们在server端添加数据源节点,把每一个读写请求都重定向到回调函数里。注意,这个和第二节的读之前回调、写之后回调是不一样的,后面会分析。

添加变量数据源的代码如下,

// server.c

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include 
#include 

#include "open62541.h"

UA_Boolean running = true;

static void stopHandler(int sign) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}

static UA_StatusCode readCurrentTime(UA_Server *server,
                const UA_NodeId *sessionId, void *sessionContext,
                const UA_NodeId *nodeId, void *nodeContext,
                UA_Boolean sourceTimeStamp, const UA_NumericRange *range,
                UA_DataValue *dataValue) 
{
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "read current time");
    UA_DateTime now = UA_DateTime_now();
    UA_Variant_setScalarCopy(&dataValue->value, &now,
                             &UA_TYPES[UA_TYPES_DATETIME]);
    dataValue->hasValue = true;
    return UA_STATUSCODE_GOOD;
}

static UA_StatusCode writeCurrentTime(UA_Server *server,
                 const UA_NodeId *sessionId, void *sessionContext,
                 const UA_NodeId *nodeId, void *nodeContext,
                 const UA_NumericRange *range, const UA_DataValue *data) 
{
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
                "Changing the system time is not implemented");
    return UA_STATUSCODE_BADINTERNALERROR;
}

// 添加当前时间的数据源
static void addCurrentTimeDataSourceVariable(UA_Server *server) 
{
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "Current time - data source");
    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;

    UA_NodeId currentNodeId = UA_NODEID_STRING(1, "current-time-datasource");
    UA_QualifiedName currentName = UA_QUALIFIEDNAME(1, "current-time-datasource");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_NodeId variableTypeNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);

    UA_DataSource timeDataSource;
    timeDataSource.read = readCurrentTime;
    timeDataSource.write = writeCurrentTime;
    UA_Server_addDataSourceVariableNode(server, currentNodeId, parentNodeId,
                                        parentReferenceNodeId, currentName,
                                        variableTypeNodeId, attr,
                                        timeDataSource, NULL, NULL);
}


int main(void) 
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    addCurrentTimeDataSourceVariable(server);

    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

解析

  1. 代码中使用了UA_Server_addDataSourceVariableNode()给时间变量添加数据源节点
  2. 当Client去读系统时间时,就会转去调用数据源节点的read函数,也就是readCurrentTime(),该函数直接读取当前系统时间,并赋值给目标参数,用于返回给Client
  3. 这个和第二节的读之前回调不一样,那个是获取当前系统时间,然后把实际值赋值给变量节点,最后client再获取变量节点的值,多了一步操作步骤

编译后运行,使用UaExpert进行连接,显示如下,
学习open62541 --- [5] Server连接变量和物理过程_第6张图片
点击一次“Current time - data source”就可以获取当前的系统时间,
学习open62541 --- [5] Server连接变量和物理过程_第7张图片
在server端也会有打印,
在这里插入图片描述
当点击别的地方后再点回来,可以发现时间又update了,
学习open62541 --- [5] Server连接变量和物理过程_第8张图片
同样server端也会再次打印。

数据源的更详细使用方法请阅读这篇文章。


四 总结

本文以系统时间举例,讲述如何在OPC UA Server里把变量和物理过程联系起来,可以看到总共有3种方法:手动更新,变量值回调和变量数据源,其中手动更新需要消耗很多资源,变量值回调和变量数据源则会更加省资源,而且变量数据源相比变量值回调则是更直接一点。实际使用过程中可以根据需要自行选择。

如果有写的不对的地方,希望能留言指正,谢谢阅读。

你可能感兴趣的:(open62541,C/C++)