本节主要讲述如何在OPC UA Server中添加对象并自定义对象类型。
一 构建对象节点
假设有一个学生对象,一般来说一个学生都会有以下公共信息:姓名,性别,年龄,身高,体重。根据面向对象的定义,每个学生都可以用一个对象来表示,其结构如下,
简单说下这幅图,Student是一个对象节点,Name是变量节点,它俩之间的关系是hasComponent,这个hasComponent是OPC UA信息模型中的一种,简单来说就是包含的关系,Student包含Name,其它变量节点类似。通过hasComponent就可以组成一个Student对象。
下面让我们把这个对象节点添加到OPC UA Server里
手动添加Student对象
代码如下,
// 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;
}
static void manuallyDefineStudent(UA_Server * server)
{
UA_NodeId studentId; /* get the nodeid assigned by the server */
UA_ObjectAttributes stuAttr = UA_ObjectAttributes_default;
stuAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Student (Manual)");
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "Student (Manual)"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
stuAttr, NULL, &studentId);
// 添加姓名
UA_VariableAttributes nameAttr = UA_VariableAttributes_default;
UA_String studentName = UA_STRING("Xiao Ming");
UA_Variant_setScalar(&nameAttr.value, &studentName, &UA_TYPES[UA_TYPES_STRING]);
nameAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Name");
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "StudentName"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), nameAttr, NULL, NULL);
// 添加性别
UA_VariableAttributes genderAttr = UA_VariableAttributes_default;
UA_String gender = UA_STRING("Male");
UA_Variant_setScalar(&genderAttr.value, &gender, &UA_TYPES[UA_TYPES_STRING]);
genderAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Gender");
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Gender"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), genderAttr, NULL, NULL);
// 添加年龄
UA_VariableAttributes ageAttr = UA_VariableAttributes_default;
UA_Byte age = 16;
UA_Variant_setScalar(&ageAttr.value, &age, &UA_TYPES[UA_TYPES_BYTE]);
ageAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Age");
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Age"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), ageAttr, NULL, NULL);
// 添加身高
UA_VariableAttributes heightAttr = UA_VariableAttributes_default;
UA_UInt16 height = 170;
UA_Variant_setScalar(&heightAttr.value, &height, &UA_TYPES[UA_TYPES_UINT16]);
heightAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Height (cm)");
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Height (cm)"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), heightAttr, NULL, NULL);
// 添加体重
UA_VariableAttributes weightAttr = UA_VariableAttributes_default;
UA_UInt16 weight = 60;
UA_Variant_setScalar(&weightAttr.value, &weight, &UA_TYPES[UA_TYPES_UINT16]);
weightAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Weight (kg)");
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Weight (kg)"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), weightAttr, NULL, NULL);
}
int main(void)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
manuallyDefineStudent(server);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
代码解释:
- 先用UA_Server_addObjectNode()创建Student对象节点
- 然后使用UA_Server_addVariableNode()创建5个变量节点
- 变量节点的parentNodeId是Student对象节点的Id,它们的关系是hasComponent
编译运行后,使用UaExpert连接server,显示如下,
在Reference窗口中可以看到对象节点和变量节点之间的关系,即HasComponent
二 自定义对象类型
上一节我们手动添加了一个学生对象节点,可以看出来需要写很多代码,如果有100个学生对象,那么代码量就很大而且都是重复,假如有一个学生对象的类型,就可以直接使用类型去生成对象,无需编写这么多代码了,这是一种OOP的思想。
对于OPC UA来说类型也是一个节点,所以类型是节点,对象是节点,变量也是节点,节点间使用信息模型的各种关系来互相连接,如前面的hasComponent。
定义类型之前,先规划一下Student类型节点,如下,
注意:Student现在是ObjectTypeNode,而对于姓名、性别和年龄,这三项的ModellingRule是mandatory的,意思是创建对象时这几个变量必须生成。关于ModellingRule可以参考OPC UA文档Part3
下面是相关代码,
// 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;
}
/* predefined identifier for later use */
UA_NodeId studentTypeId = {1, UA_NODEIDTYPE_NUMERIC, {1001}};
static void defineObjectType(UA_Server *server)
{
/* Define the object type for "Student" */
UA_ObjectTypeAttributes stuAttr = UA_ObjectTypeAttributes_default;
stuAttr.displayName = UA_LOCALIZEDTEXT("en-US", "StudentType");
UA_Server_addObjectTypeNode(server, studentTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
UA_QUALIFIEDNAME(1, "StudentType"), stuAttr,
NULL, NULL);
// 添加姓名
UA_VariableAttributes nameAttr = UA_VariableAttributes_default;
UA_String studentName = UA_STRING("Unknown");
UA_Variant_setScalar(&nameAttr.value, &studentName, &UA_TYPES[UA_TYPES_STRING]); // 初始化为Unknown,后续再修改
nameAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Name");
nameAttr.accessLevel |= UA_ACCESSLEVELMASK_WRITE; // default是只读,添加写权限
UA_NodeId nameId;
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Name"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), nameAttr, NULL, &nameId);
/* Make the student name mandatory */
UA_Server_addReference(server, nameId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
// 添加性别
UA_VariableAttributes genderAttr = UA_VariableAttributes_default;
UA_String gender = UA_STRING("Unknown");
UA_Variant_setScalar(&genderAttr.value, &gender, &UA_TYPES[UA_TYPES_STRING]); // 初始化为Unknown,后续再修改
genderAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Gender");
genderAttr.accessLevel |= UA_ACCESSLEVELMASK_WRITE; // default是只读,添加写权限
UA_NodeId genderId;
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Gender"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), genderAttr, NULL, &genderId);
/* Make the student gender mandatory */
UA_Server_addReference(server, genderId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
// 添加年龄
UA_VariableAttributes ageAttr = UA_VariableAttributes_default;
UA_Byte age = 0;
UA_Variant_setScalar(&ageAttr.value, &age, &UA_TYPES[UA_TYPES_BYTE]); // 初始化为0,后续再修改
ageAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Age");
ageAttr.accessLevel |= UA_ACCESSLEVELMASK_WRITE; // default是只读,添加写权限
ageAttr.valueRank = UA_VALUERANK_SCALAR;
UA_NodeId ageId;
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Age"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), ageAttr, NULL, &ageId);
/* Make the student age mandatory */
UA_Server_addReference(server, ageId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASMODELLINGRULE),
UA_EXPANDEDNODEID_NUMERIC(0, UA_NS0ID_MODELLINGRULE_MANDATORY), true);
// 添加身高
UA_VariableAttributes heightAttr = UA_VariableAttributes_default;
UA_UInt16 height = 0;
UA_Variant_setScalar(&heightAttr.value, &height, &UA_TYPES[UA_TYPES_UINT16]); // 初始化为0,后续再修改
heightAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Height (cm)");
heightAttr.accessLevel |= UA_ACCESSLEVELMASK_WRITE; // default是只读,添加写权限
heightAttr.valueRank = UA_VALUERANK_SCALAR;
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Height (cm)"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), heightAttr, NULL, NULL);
// 添加体重
UA_VariableAttributes weightAttr = UA_VariableAttributes_default;
UA_UInt16 weight = 0;
UA_Variant_setScalar(&weightAttr.value, &weight, &UA_TYPES[UA_TYPES_UINT16]); // 初始化为0,后续再修改
weightAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Weight (kg)");
weightAttr.accessLevel |= UA_ACCESSLEVELMASK_WRITE; // default是只读,添加写权限
weightAttr.valueRank = UA_VALUERANK_SCALAR;
UA_Server_addVariableNode(server, UA_NODEID_NULL, studentTypeId,
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
UA_QUALIFIEDNAME(1, "Weight (kg)"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), weightAttr, NULL, NULL);
}
static int getChildIdSimplified(UA_Server *server,
UA_NodeId parentNode,
const int relativePathCnt,
const UA_QualifiedName targetNameArr[],
UA_NodeId *result)
{
int ret = 0;
UA_BrowsePathResult bpr = UA_Server_browseSimplifiedBrowsePath(server,
parentNode, relativePathCnt, targetNameArr);
if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1)
{
printf("error: %s\n", UA_StatusCode_name(bpr.statusCode));
ret = -1;
}
else
{
UA_NodeId_copy(&bpr.targets[0].targetId.nodeId, result);
}
UA_BrowsePathResult_deleteMembers(&bpr);
return ret;
}
struct stuInitInfo
{
UA_String name;
UA_String gender;
UA_Byte age;
};
static void createStudentObjectInstance(UA_Server *server, char *name, struct stuInitInfo *info)
{
UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
oAttr.displayName = UA_LOCALIZEDTEXT("en-US", name);
UA_NodeId retId;
UA_Server_addObjectNode(server, UA_NODEID_NULL,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, name),
studentTypeId, /* this refers to the object type identifier */
oAttr, NULL, &retId);
UA_NodeId itemId;
UA_Variant value;
UA_QualifiedName targetNameArr[1];
targetNameArr[0] = UA_QUALIFIEDNAME(1, "Name");
if (getChildIdSimplified(server, retId, 1, targetNameArr, &itemId) == 0)
{
UA_Variant_init(&value);
UA_Variant_setScalar(&value, &info->name, &UA_TYPES[UA_TYPES_STRING]);
UA_Server_writeValue(server, itemId, value);
}
targetNameArr[0] = UA_QUALIFIEDNAME(1, "Gender");
if (getChildIdSimplified(server, retId, 1, targetNameArr, &itemId) == 0)
{
UA_Variant_init(&value);
UA_Variant_setScalar(&value, &info->gender, &UA_TYPES[UA_TYPES_STRING]);
UA_Server_writeValue(server, itemId, value);
}
targetNameArr[0] = UA_QUALIFIEDNAME(1, "Age");
if (getChildIdSimplified(server, retId, 1, targetNameArr, &itemId) == 0)
{
UA_Variant_init(&value);
UA_Variant_setScalar(&value, &info->age, &UA_TYPES[UA_TYPES_BYTE]);
UA_Server_writeValue(server, itemId, value);
}
}
int main(void)
{
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new();
UA_ServerConfig_setDefault(UA_Server_getConfig(server));
defineObjectType(server);
struct stuInitInfo initInfo = {};
initInfo.name = UA_STRING("Xiao Ming");
initInfo.gender = UA_STRING("Male");
initInfo.age = 20;
createStudentObjectInstance(server, "Xiao Ming", &initInfo);
initInfo.name = UA_STRING("Xiao Hong");
initInfo.gender = UA_STRING("Female");
initInfo.age = 22;
createStudentObjectInstance(server, "Xiao Hong", &initInfo);
initInfo.name = UA_STRING("Zhang San");
initInfo.gender = UA_STRING("Male");
initInfo.age = 18;
createStudentObjectInstance(server, "Zhang San", &initInfo);
UA_StatusCode retval = UA_Server_run(server, &running);
UA_Server_delete(server);
return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}
代码解释:
- defineObjectType()定义了学生类型节点,其Id是我们预先定义好的,即studentTypeId
- 使用UA_Server_addReference()来设置变量的ModellingRule
- 在createStudentObjectInstance()里使用了UA_Server_addObjectNode()创建对象,在对象类型的参数里传递了studentTypeId,具体可以去看UA_Server_addObjectNode()的参数定义
- 在createStudentObjectInstance()里使用getChildIdSimplified()来获取姓名,性别和年龄的NodeId,然后根据这个去设置其初始值
- 变量的accessLevel可以根据需要决定是否添加写权限,否则默认是只读的
编译运行,然后使用UaExpert来连接查看,如下,
注意,创建对象时只会创建ModellingRule为mandatory的变量节点,UaExpert这里只显示了Age,Gender和Name
我们定义的学生类型节点在哪里呢?根据代码,学生类型节点是在UA_NS0ID_BASEOBJECTTYPE之下的,和其关系是UA_NS0ID_HASSUBTYPE,即学生类型是其子类型。
我们先找到BaseObjectType,如下,
然后展开就可以看到我们定义的学生类型了,
在学生类型下再点击Age、Gender等,在右侧References窗口里可以查看它们之间的关系,可以看到ModellingRule是否是mandatory的,如下图
这样我们就自定义了学生类型,并用这个类型创建了三个学生对象,然后添加到OPC UA Server里。
还有一点需要注意,生成的对象及其包含的变量,它们的NodeId都是由OPC UA Server运行时随机分配的,这样在编写client端代码时就无法确切的获取NodeId,可以参照这篇文章来解决这个问题,本文代码使用的函数getChildIdSimplified()也是来源于该文章。
对于不是Mandatory的成员变量如何实例化,可以参考这篇文章。
三 总结
本文主要讲述了如何创建对象节点以及如何自定义对象类型并用来创建对象。这样我们就可以随意的在OPC UA Server端创建各种对象了。
本文主要参考open62541的官网文档,如果有写的不对的地方,希望能留言指正,谢谢阅读。