本文主要讲述如何在OPC UA Server里添加方法,以及如何调用添加的方法。
一 添加HelloWorld方法
现在我们来添加一个HelloWorld方法,其有一个输入参数和一个输出参数,代码如下,
// 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 <signal.h>
#include <stdlib.h>
#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;
}
/*
* 该方法的功能是当输入一个字符串XX,就回复“Hello XX”
*/
static UA_StatusCode helloWorldMethodCallback(UA_Server *server,
const UA_NodeId *sessionId, void *sessionHandle,
const UA_NodeId *methodId, void *methodContext,
const UA_NodeId *objectId, void *objectContext,
size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output)
{
UA_String *inputStr = (UA_String*)input->data;
UA_String tmp = UA_STRING_ALLOC("Hello ");
if(inputStr->length > 0) {
tmp.data = (UA_Byte *)UA_realloc(tmp.data, tmp.length + inputStr->length);
memcpy(&tmp.data[tmp.length], inputStr->data, inputStr->length);
tmp.length += inputStr->length;
}
UA_Variant_setScalarCopy(output, &tmp, &UA_TYPES[UA_TYPES_STRING]);
UA_String_clear(&tmp);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Hello World was called");
return UA_STATUSCODE_GOOD;
}
static void addHellWorldMethod(UA_Server *server)
{
UA_Argument inputArgument;
UA_Argument_init(&inputArgument);
inputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String");
inputArgument.name = UA_STRING("MyInput");
inputArgument.dataType = UA_TYPES[UA_TYPES_STRING].typeId;
inputArgument.valueRank = UA_VALUERANK_SCALAR;
UA_Argument outputArgument;
UA_Argument_init(&outputArgument);
outputArgument.description = UA_LOCALIZEDTEXT("en-US", "A String");
outputArgument.name = UA_STRING("MyOutput");
outputArgument.dataType = UA_TYPES[UA_TYPES_STRING].typeId;
outputArgument.valueRank = UA_VALUERANK_SCALAR;
UA_MethodAttributes helloAttr = UA_MethodAttributes_default;
helloAttr.description = UA_LOCALIZEDTEXT("en-US","Say `Hello World`");
helloAttr.displayName = UA_LOCALIZEDTEXT("en-US","Hello World");
helloAttr.executable = true;
helloAttr.userExecutable = true;
UA_Server_addMethodNode(server, UA_NODEID_NUMERIC(1,62541),
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASORDEREDCOMPONENT),
UA_QUALIFIEDNAME(1, "hello world"),
helloAttr, &helloWorldMethodCallback,
1, &inputArgument, 1, &outputArgument, NULL, NULL);
}
int main(void)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
addHellWorldMethod(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
代码中只有2个函数,helloWorldMethodCallback()和addHellWorldMethod(),下面简单解析下,
helloWorldMethodCallback()解析:
- 函数功能是当输入一个字符串“XX”,就返回“Hello XX”。所以需要一个输入参数和一个输出参数。
- 参数列表里的inputSize和input是一对,input可以看做是指向输入参数数组的指针,inputSize则表示输入参数数组中元素的个数,outputSize和output是用于存放输出参数。
- OPC UA Server中方法的回调函数,其参数列表必须和本函数一样。
addHellWorldMethod()解析:
- 定义了一个输入参数对象和一个输出参数对象,并设置了其名字、数据类型和取值范围等
- 定义HelloWorld方法的属性,例如设置其显示名称为“Hello World”以及是否可执行等
- 使用UA_Server_addMethodNode()添加HelloWorld方法,传入了输入参数对象和输出参数对象以及helloWorldMethodCallback,并设置该方法的节点id是62541
- HelloWorld方法添加到Objects目录下,它与Objects目录的关系是HasOrderedComponent
当调用HelloWorld方法时,会把实参传给这个输入参数对象,然后执行helloWorldMethodCallback,并把结果存放到输出参数对象里返回
编译运行,并使用UaExpert去连接,显示如下,
表示OPC UA Server添加方法成功,可见方法在OPC UA Server中也是一个Node。
二 调用HelloWorld方法
下面看下如何调用上节添加的HelloWorld方法
1. 使用UaExpert调用
右击该方法,选择“Call”
弹出如下函数调用界面,界面上的显示很多都是之前代码里设置的名称,如MyInput,MyOutput等
在MyInput里随意输入一个字符串(MyInput是输入参数名字,是前面代码里设置的),例如“everyone”,然后点击Call按钮,
可以看到输出参数MyOutput里出现了“Hello everyone”,说明HelloWorld方法调用成功。
2. 编写OPC UA Client代码去调用
HelloWorld方法的NodeId是62541,所以我们可以使用UA_Client_call()来调用这个HelloWorld方法,代码如下,
#include <stdlib.h>
#include "open62541.h"
int main(void)
{
UA_Client *client = UA_Client_new();
UA_ClientConfig_setDefault(UA_Client_getConfig(client));
UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
if(retval != UA_STATUSCODE_GOOD)
{
UA_Client_delete(client);
return (int)retval;
}
/* Call a remote method */
UA_Variant input;
UA_String argString = UA_STRING("everyone");
UA_Variant_init(&input);
UA_Variant_setScalarCopy(&input, &argString, &UA_TYPES[UA_TYPES_STRING]);
size_t outputSize;
UA_Variant *output;
retval = UA_Client_call(client, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(1, 62541), 1, &input, &outputSize, &output);
if(retval == UA_STATUSCODE_GOOD)
{
printf("Method call was successful, and %lu returned values available.\n",
(unsigned long)outputSize);
UA_String outString = *(UA_String*)output->data;
printf("%.*s\n", outString.length, outString.data);
UA_Array_delete(output, outputSize, &UA_TYPES[UA_TYPES_VARIANT]);
}
else
{
printf("Method call was unsuccessful, and %x returned values available.\n", retval);
}
UA_Variant_clear(&input);
/* Clean up */
UA_Client_delete(client); /* Disconnects the client internally */
return EXIT_SUCCESS;
}
代码中的printf()第一个参数是“%.*s”,这是一个小技巧,意思是打印指定长度的字符串,printf的第2个参数就是字符串长度。
编译并运行(注意,先要运行server),得到结果如下,
三 方法和对象之间的关系
在OPC UA信息模型中,有些对象会包含方法,这个和面向对象编程中对象类型中包含方法是类似的。
如果一个OPC UA对象类型里包含方法,那么当使用这个对象类型去实例化对象时就会给这个对象添加该方法的引用,而不是把这个方法拷贝给对象,使用相同类型创建的所有对象,它们的方法都会指向同一个方法,例子可以参考这篇文章。
四 总结
本文主要讲述如何在OPC UA Server中添加方法,这个功能很重要也非常有用。
另外本文添加的方法只有一个输入参数和一个输出参数,如果想要多个输入参数和多个输出参数,可以根据本文例子去修改。
如果有写的不对的地方,希望能留言指正,谢谢阅读。