本人最近遇到一个问题:给一个变量添加数据源后,使用监测项去监测变量变化,如果采样时间为0,会发现无法监测到变量的变化。
本文讲述这种情况的发生原因以及解决办法。
一 Server例子
首先准备server例子,如下,
// server.c
#include <signal.h>
#include <stdlib.h>
#include "open62541.h"
UA_Boolean running = true;
void stopHandler(int sign) {
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
running = false;
}
int32_t gData = 0;
UA_NodeId gTargetNodeId = UA_NODEID_STRING(1, (char*)"the.answer");
void addVariable(UA_Server * server)
{
/* Define the attribute of the myInteger variable node */
UA_VariableAttributes attr = UA_VariableAttributes_default;
UA_Int32 myInteger = 0;
UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
attr.description = UA_LOCALIZEDTEXT((char*)"en-US", (char*)"the answer");
attr.displayName = UA_LOCALIZEDTEXT((char*)"en-US", (char*)"the answer");
attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId;
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
/* Add the variable node to the information model */
UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, (char*)"the answer");
UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
UA_Server_addVariableNode(server, gTargetNodeId, parentNodeId,
parentReferenceNodeId, myIntegerName,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), attr, NULL, NULL);
}
UA_StatusCode onReadCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *nodeId,
void *nodeContext, UA_Boolean includeSourceTimeStamp,
const UA_NumericRange *range, UA_DataValue *value)
{
UA_Variant_setScalarCopy(&(value->value), &gData, &UA_TYPES[UA_TYPES_INT32]);
value->hasValue = UA_TRUE;
return UA_STATUSCODE_GOOD;
}
UA_StatusCode onWriteCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *nodeId,
void *nodeContext, const UA_NumericRange *range,
const UA_DataValue *value)
{
return UA_STATUSCODE_GOOD;
}
void addDataSourceToVariable(UA_Server *server)
{
UA_DataSource varDataSource;
varDataSource.read = onReadCallback;
varDataSource.write = onWriteCallback;
UA_Server_setVariableNode_dataSource(server, gTargetNodeId, varDataSource);
}
void cycleCallback(UA_Server *server, void *data)
{
if ((++gData) == 10000)
{
gData = 0;
}
}
int main(void)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addVariable(server);
addDataSourceToVariable(server);
UA_UInt64 callbackId = 0;
UA_Server_addRepeatedCallback(server, cycleCallback, NULL, 1000, &callbackId); // call every 1s
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
代码里添加了一个变量叫“the answer”,并添加了数据源用于读写操作,代码里有个全局变量gData,是“the answer”的源,每隔2秒会加1,当用户读取变量值时,会调用读回调onReadCallback,然后把gData的值传给用户。
二 测试
a)使用UaExpert的默认配置进行监测
编译server后运行,然后使用UaExpert连接server,
然后把变量“the answer”拖到Data Access View里,
可以看到值每隔1s就会加1,和预期一样,在server这边,会打印监测项的配置,如下,
可以看到采样时间是250ms,这个是UaExpert的默认配置
b)使用有变化才通知的监测设置
有时用户希望有变化才通知,不想按照一定时间间隔去采样,这样就需要对UaExpert进行配置,点击Setting,然后点击Configure UaExpert,如下,
在弹出的设置框里把采样时间的默认值改为0,
然后点击右下角的OK进行保存。
最后重启一下server,然后再使用UaExpert进行连接,连接成功后再把变量"the answer"拖拽到Data Access View窗口里,此时会发现数据不在变化了。
在Server这端可以看到这个监测项的设置,如下,采样时间是0
三 原因分析
由上可以看出,设置250ms采样,就可以监测到数据变化,而设置为有变化再通知(采样时间为0),就无法监测到数据的变化,其根本原因是:
- 250ms采样,server内部会每隔250ms去读一下变量的值,这就会调用onReadCallback,这样就能拿到变化的值
- 0ms采样,server内部不会去调用读接口去读取变量值,而是在对变量进行写的时候才去检查一下变量值是否发生变化,这样才能做到有变化才通知
server源码中创建监测项的的函数如下,
UA_StatusCode
UA_MonitoredItem_registerSampling(UA_Server *server, UA_MonitoredItem *mon) {
UA_LOCK_ASSERT(&server->serviceMutex, 1);
if(mon->sampleCallbackIsRegistered)
return UA_STATUSCODE_GOOD;
UA_assert(mon->next == (UA_MonitoredItem*)~0); /* Not registered in a node */
/* Only DataChange MonitoredItems with a positive sampling interval have a
* repeated callback. Other MonitoredItems are attached to the Node in a
* linked list of backpointers. */
UA_StatusCode res;
if(mon->itemToMonitor.attributeId == UA_ATTRIBUTEID_EVENTNOTIFIER ||
mon->parameters.samplingInterval == 0.0) {
UA_Subscription *sub = mon->subscription;
UA_Session *session = &server->adminSession;
if(sub)
session = sub->session;
res = UA_Server_editNode(server, session, &mon->itemToMonitor.nodeId,
addMonitoredItemBackpointer, mon);
} else {
res = addRepeatedCallback(server,
(UA_ServerCallback)UA_MonitoredItem_sampleCallback,
mon, mon->parameters.samplingInterval,
&mon->sampleCallbackId);
}
if(res == UA_STATUSCODE_GOOD)
mon->sampleCallbackIsRegistered = true;
return res;
}
可以看到采样时间不为0,就会创建一个循环执行的任务,每隔一段时间就去读一下值。
如果采样时间为0,就会把这个监测项添加到node里的一个单链表里,该链表存放所有对该node创建的检测项(因为代码里可能会对同一个变量添加多个监测项),当对变量进行写时就会去检查是否发生变化并逐个进行通知,此时所使用的函数如下,
/* Trigger sampling if a MonitoredItem surveils the attribute with no sampling
* interval */
#ifdef UA_ENABLE_SUBSCRIPTIONS
static void
triggerImmediateDataChange(UA_Server *server, UA_Session *session,
UA_Node *node, const UA_WriteValue *wvalue) {
for(UA_MonitoredItem *mon = node->head.monitoredItems; mon != NULL; mon = mon->next) {
if(mon->itemToMonitor.attributeId != wvalue->attributeId)
continue;
UA_DataValue value;
UA_DataValue_init(&value);
ReadWithNode(node, server, session, mon->timestampsToReturn,
&mon->itemToMonitor, &value);
UA_Subscription *sub = mon->subscription;
UA_StatusCode res = sampleCallbackWithValue(server, sub, mon, &value);
if(res != UA_STATUSCODE_GOOD) {
UA_DataValue_clear(&value);
UA_LOG_WARNING_SUBSCRIPTION(&server->config.logger, sub,
"MonitoredItem %" PRIi32 " | "
"Sampling returned the statuscode %s",
mon->monitoredItemId,
UA_StatusCode_name(res));
}
}
}
#endif
四 解决办法
知道了原因,解决办法也比较简单,就是当源变化时,要更新一下对应变量的值,对应到代码里,就是在cycleCallback()里把gData的当前值写入到变量里,如下,
void cycleCallback(UA_Server *server, void *data)
{
if ((++gData) == 10000)
{
gData = 0;
}
UA_Variant value;
UA_Variant_setScalarCopy(&(value), &gData, &UA_TYPES[UA_TYPES_INT32]);
UA_Server_writeValue(server, gTargetNodeId, value);
}
这样就可以触发通知了,重新编译运行,就会发现可以监测到变量的变化了。