UaModeler是一个OPC UA信息模型的建模工具,和UaExpert同出一个网站,可以去其网站下载(需要注册一个账号),也可以点击这里进行下载(本人下载后传到百度云上)。注意,这是个商业软件,免费使用时可创建的Node数量有限,不过用来学习足够了。
使用SIOME建模的文章请点击这里,西门子出的免费软件,也非常好用。
在之前的系列文章中,我们往OPC UA Server里添加东西都是使用代码,当工程比较大时这么做就有点繁琐了。本文主要讲述如何使用UaModeler进行建模,并在代码中使用建好的模型。
下载后解压,然后点击这个exe文件进行安装,
安装ok后打开,界面如下,
在Help下有个Handbook,里面描述了常用的使用方法,大家也可以直接参考这个手册。
点击File->New Project,
在弹出的界面里,输入Project Name并选择工程保存位置,然后点击Next,
在下一个Generate Code界面里,选择生成的代码类型,这里选择ansi_c v1_9,然后再选择一下输出代码的路径,最后点击Next,(代码类型可随意选,我们最后并不使用UaModeler去生成代码)
后面2个界面选Next就行了,使用默认配置,
然后下一个界面里可以根据需要修改Namespace URI,最后点击Finish
工程新建好后,界面如下,
我们新建的model在Projet窗口下,即example.ua
我们想新增一个Object Type,即对象类型,该类型含有2个变量和一个方法,这个方法有2个输入参数和一个输出参数。
在Information Model下展开Types->ObjectTypes,选中BaseObjectType,然后右击,
点击Add New Type,在中间的窗口中输入想要添加的Object Type名称,这里填入MyObjectType,
然后点击Children右侧的那个倒三角进行展开,
点击Select NodeClass,选择Variable,
然后在Name栏下输入名字Var1,并更改DataType为Int32,
先点击Double右侧的那个下拉符号,下拉后点击最下面的Add,
在弹出的界面里选择数据类型,
同样,我们再添加一个Variable,叫Var2,
再添加一个方法,
名字叫Func,
展开这个Method,设置其输入和输出参数,
这里为这个方法设置2个输入参数:input0和input1,1个输出参数:output0,类型都是Int32,如下图(注意:只要Add Argument里没有输入名称,那么这个就不算作参数)
对于添加的方法,还需要注意一点:我们并没有为方法添加代码逻辑,只能算一个空壳,后面在使用这个Object Type生成对象时才会添加逻辑。
其它都采取默认,然后保存工程,这样这个Object Type就创建好了。
使用UaModeler建模只需要最终生成的xml,不需要其生成的代码,后续会使用open62541自带的工具生成相关代码。
在Project窗口选中example.ua,右击,选择Export XML(第一次会询问是否要设置模型版本号,可以设置也可以不设置,采取默认也行)
生成OK后,在工程目录下可以看到生成的xml文件,如下,
首先,需要对open62541进行配置,先打开dos窗口或shell窗口,cd到open62541源码目录下,执行下面的命令,
git submodule update --init
会下载一些必须的子模块,用于代码生成。
然后,打开open62541源码目录下的CMakeLists.txt,找到UA_ENABLE_AMALGAMATION设置为ON,接着找到下面这段设置,
# Namespace Zero
set(UA_NAMESPACE_ZERO "REDUCED" CACHE STRING "Completeness of the generated namespace zero (minimal/reduced/full)")
SET_PROPERTY(CACHE UA_NAMESPACE_ZERO PROPERTY STRINGS "MINIMAL" "REDUCED" "FULL")
把UA_NAMESPACE_ZERO的值由REDUCED改为FULL,然后执行以下操作,
cmake .. && make
,会比较耗时OK后把open62541.h和libopen62541.a拷贝到自定义工程目录,例如如下,
myNS是本次的工程目录,也可以根据需要自定义任意目录
PS:由于UA_NAMESPACE_ZERO变成FULL,所以libopen62541.a也变大了很多
这一步就使用到了之前生成的example.xml,先把该xml文件拷贝到tools/nodeset_compiler下,然后执行下面的命令,最后一个参数myNS用来指示生成的代码文件名称,
python ./nodeset_compiler.py --types-array=UA_TYPES --existing ../../deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml --xml example.xml myNS
打印如下,表示生成成功
在当前路径下输入ls,可以看到生成了myNS.c和myNS.h,这2个文件就是我们需要的,
把myNS.c和myNS.h拷贝到如下src目录,
打开myNS.h,其中有段编译控制,
#ifdef UA_ENABLE_AMALGAMATION
# include "open62541.h"
#else
# include
#endif
直接改成如下,因为我们使用的是open62541.h
# include "open62541.h"
在src目录下添加文件server.c,
其内容如下,创建了2个对象,分别叫myNSObject和myNSObject2,
/* 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"
/* Files myNS.h and myNS.c are created from myNS.xml */
#include "myNS.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 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_Int32 value = 0;
for (size_t i = 0; i < inputSize; ++i)
{
UA_Int32 * ptr = (UA_Int32 *)input[i].data;
value += (*ptr);
}
UA_Variant_setScalarCopy(output, &value, &UA_TYPES[UA_TYPES_INT32]);
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Hello World was called");
return UA_STATUSCODE_GOOD;
}
int main(int argc, char **argv)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
UA_StatusCode retval;
/* create nodes from nodeset */
if (myNS(server) != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Could not add the example nodeset. "
"Check previous output for any error.");
retval = UA_STATUSCODE_BADUNEXPECTEDERROR;
} else {
// 方法节点的NodeId是UA_NODEID_NUMERIC(2, 7001)
UA_Server_setMethodNode_callback(server, UA_NODEID_NUMERIC(2, 7001), &helloWorldMethodCallback);
UA_NodeId createdNodeId;
UA_ObjectAttributes object_attr = UA_ObjectAttributes_default;
object_attr.description = UA_LOCALIZEDTEXT("en-US", "myNSObject");
object_attr.displayName = UA_LOCALIZEDTEXT("en-US", "myNSObject");
// we assume that the myNS nodeset was added in namespace 2.
// You should always use UA_Server_addNamespace to check what the
// namespace index is for a given namespace URI. UA_Server_addNamespace
// will just return the index if it is already added.
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "myNSObject"),
UA_NODEID_NUMERIC(2, 1002),
object_attr, NULL, &createdNodeId);
UA_NodeId createdNodeId2;
UA_ObjectAttributes object_attr2 = UA_ObjectAttributes_default;
object_attr2.description = UA_LOCALIZEDTEXT("en-US", "myNSObject2");
object_attr2.displayName = UA_LOCALIZEDTEXT("en-US", "myNSObject2");
// we assume that the myNS nodeset was added in namespace 2.
// You should always use UA_Server_addNamespace to check what the
// namespace index is for a given namespace URI. UA_Server_addNamespace
// will just return the index if it is already added.
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "myNSObject2"),
UA_NODEID_NUMERIC(2, 1002),
object_attr2, NULL, &createdNodeId2);
retval = UA_Server_run(server, &running);
}
UA_Server_delete(server);
return (int) retval;
}
代码解析:
可能会问:我怎么知道对象类型的NodeId以及其方法的NodeId呢?有2种方法:
如果是正式应用,推荐第2种方法去获得NodeId
cmake_minimum_required(VERSION 3.5)
project(myNamespace)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
add_definitions(-std=c99)
include_directories(${PROJECT_SOURCE_DIR}/open62541)
include_directories(${PROJECT_SOURCE_DIR}/src)
link_directories(${PROJECT_SOURCE_DIR}/open62541)
add_executable(server ${PROJECT_SOURCE_DIR}/src/server.c ${PROJECT_SOURCE_DIR}/src/myNS.c)
target_link_libraries(server libopen62541.a)
cd到build目录下执行cmake .. && make
,生成的elf文件在bin目录下,由于libopen62541.a变大了,所以链接时会比较慢。
连接OK后,可以看到创建的2个对象都成功生成了。
展开这2个对象,可以看到它们的方法Func的NodeId都是一样的,而变量的则是不同的,这也印证了前面的说法,
可以执行一下这个方法来测试一下,右击Func,点击Call,
在弹出的界面里输入2个参数值,然后点击Call,
最后会在输出参数里得到300,和期望的一样
验证OK!
本文主要讲述如何使用UaModeler来创建信息模型,然后生成对应的xml文件,最后使用open62541自带的工具把信息模型转成代码并添加到OPC UA Server里。
过程稍微复杂了一点,本人写的也是累的一批。希望看过的同学能给个赞,谢谢。
本文创建的对象类型比较简单,如果需要创建复杂的类型或多个类型,则需要自己探索,如果搞懂了本文的例子,应该没有什么问题。
如果有写的不对的地方,希望能留言指正,谢谢阅读。