在之前的一篇文章中,讲了如何添加变量类型。本文讲述如何自定义数据类型,这里要弄清楚一个概念:变量类型(Variable Type)和数据类型(Data Type)是不一样的,变量类型基于数据类型,我们平时创建变量时,是使用变量类型来创建的。

一般来说,自定义数据类型都是结构体。本文讲述如何把各种不同种类的结构体类型作为数据类型添加到Server中来,并使用它们,主要参考open62541自带example。


一 所使用的OPC UA类型

主要使用了UA_DataType和UA_DataTypeMember这2种类型,原型定义如下,元素意义参考注释,

typedef struct {
#ifdef UA_ENABLE_TYPEDESCRIPTION
    const char *memberName;
#endif
    UA_UInt16 memberTypeIndex;    /* Index of the member in the array of data
                                     types */
    UA_Byte   padding;            /* How much padding is there before this
                                     member element? For arrays this is the
                                     padding before the size_t length member.
                                     (No padding between size_t and the
                                     following ptr.) */
    UA_Boolean namespaceZero : 1; /* The type of the member is defined in
                                     namespace zero. In this implementation,
                                     types from custom namespace may contain
                                     members from the same namespace or
                                     namespace zero only.*/
    UA_Boolean isArray       : 1; /* The member is an array */
    UA_Boolean isOptional    : 1; /* The member is an optional field */
} UA_DataTypeMember;


struct UA_DataType {
#ifdef UA_ENABLE_TYPEDESCRIPTION
    const char *typeName;
#endif
    UA_NodeId typeId;                /* The nodeid of the type */
    UA_UInt16 memSize;               /* Size of the struct in memory */
    UA_UInt16 typeIndex;             /* Index of the type in the datatypetable */
    UA_UInt32 typeKind         : 6;  /* Dispatch index for the handling routines */
    UA_UInt32 pointerFree      : 1;  /* The type (and its members) contains no
                                      * pointers that need to be freed */
    UA_UInt32 overlayable      : 1;  /* The type has the identical memory layout
                                      * in memory and on the binary stream. */
    UA_UInt32 membersSize      : 8;  /* How many members does the type have? */
    UA_UInt32 binaryEncodingId;      /* NodeId of datatype when encoded as binary */
    //UA_UInt16  xmlEncodingId;      /* NodeId of datatype when encoded as XML */
    UA_DataTypeMember *members;
};

UA_DataType用于描述整体,UA_DataTypeMember用来描述结构体成员,UA_DataTypeMember是UA_DataType的成员。


二 简单例子

假设有个结构体Point定义如下,

typedef struct {
    UA_Float x;
    UA_Float y;
    UA_Float z;
} Point;
1. 描述成员

首先,我们使用UA_DataTypeMember去描述成员x,y和z,如下,

static UA_DataTypeMember Point_members[3] = {
    /* x */
    {
        UA_TYPENAME("x") /* .memberName */
        UA_TYPES_FLOAT,  /* .memberTypeIndex, points into UA_TYPES since namespaceZero is true */
        0,               /* .padding */
        true,            /* .namespaceZero, see .memberTypeIndex */
        false,           /* .isArray */
        false            /* .isOptional */
    },

    /* y */
    {
        UA_TYPENAME("y") /* .memberName */
        UA_TYPES_FLOAT,  /* .memberTypeIndex, points into UA_TYPES since namespaceZero is true */
        Point_padding_y, /* .padding */
        true,            /* .namespaceZero, see .memberTypeIndex */
        false,           /* .isArray */
        false            /* .isOptional */
    },
    /* z */
    {
        UA_TYPENAME("z") /* .memberName */
        UA_TYPES_FLOAT,  /* .memberTypeIndex, points into UA_TYPES since namespaceZero is true */
        Point_padding_z, /* .padding */
        true,            /* .namespaceZero, see .memberTypeIndex */
        false,           /* .isArray */
        false            /* .isOptional */
    }
};

整个描述比较简单,比较难理解的是padding,第二个成员y的padding值为Point_padding_y,其定义如下,

#define Point_padding_y offsetof(Point,y) - offsetof(Point,x) - sizeof(UA_Float)

offsetof用于获取结构体中成员的偏移量,由于x是第一个元素,所以其偏移为0。Point_padding_y就是算出x的结尾到y的开始,这段空间的大小,也就是y开始前的padding大小。

结构体成员所占内存空间由于对齐的缘故,不一定和其实际大小相符合,例如下面这种,

typedef struct {
    int32_t x;
    int8_t y;
    int32_t z;
} Temp;

由于对齐,y实际所占的内存空间不是1个字节,而是4个字节,其前面补了3个字节的padding。

所以,对于这种由于对齐可能会产生padding的结构体,需要去计算一下padding,然后赋值给UA_DataTypeMember中的pdding变量。

2. 描述整体

接下来是描述结构体的整体信息,

static const UA_DataType PointType = {
        UA_TYPENAME("Point")                /* .tyspeName */
        {1, UA_NODEIDTYPE_NUMERIC, {4242}}, /* .typeId */
        sizeof(Point),                      /* .memSize */
        0,                                  /* .typeIndex, in the array of custom types */
        UA_DATATYPEKIND_STRUCTURE,          /* .typeKind */
        true,                               /* .pointerFree */
        false,                              /* .overlayable (depends on endianness and
                                            the absence of padding) */
        3,                                  /* .membersSize */
        Point_binary_encoding_id,           /* .binaryEncodingId, the numeric
                                            identifier used on the wire (the
                                            namespaceindex is from .typeId) */
        Point_members                       /* .members */
};

其中几个元素比较关键,需要解释一下,

  • typeId:数据类型的NodeId
  • memSize:结构体的实际大小
  • typeIndex:自定义类型数组里的位置,这个数组就是struct UA_ServerConfig里的成员customDataTypes
  • typeKind:这里使用的是UA_DATATYPEKIND_STRUCTURE,表示是一个结构体
  • pointerFree:这里给的true,表示没有需要释放的指针,如果结构体里有指针,就需要设置为false
  • membersSize:成员数量,这里是3
  • binaryEncodingId:Point_binary_encoding_id实际值是1,不太清楚这是干嘛的…
  • members:成员描述,即上节定义的变量Point_members
3. 添加到地址空间

这一步很关键。首先把数据类型添加到Server的地址空间,


static void add3DPointDataType(UA_Server* server)
{
    UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point Type");

    UA_Server_addDataTypeNode(
        server, PointType.typeId, UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "3D.Point"), attr, NULL, NULL);
}

UA_Server_addDataTypeNode()的第二个参数,就是先前定义的数据类型的NodeId。数据类型的display名称是"3D Point Type",browse名称是"3D.Point"。

然后是使用这个数据类型去添加变量类型,变量类型的display名称是"3D Point",browse名称是"3D.Point",

static void
add3DPointVariableType(UA_Server *server) {
    UA_VariableTypeAttributes dattr = UA_VariableTypeAttributes_default;
    dattr.description = UA_LOCALIZEDTEXT("en-US", "3D Point");
    dattr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point");
    dattr.dataType = PointType.typeId;
    dattr.valueRank = UA_VALUERANK_SCALAR;

    Point p;
    p.x = 0.0;
    p.y = 0.0;
    p.z = 0.0;
    UA_Variant_setScalar(&dattr.value, &p, &PointType);

    UA_Server_addVariableTypeNode(server, pointVariableTypeId,
        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
        UA_QUALIFIEDNAME(1, "3D.Point"),
        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
        dattr, NULL, NULL);

}

关键点是dattr.dataType传递的是PointType.typeId,即定义变量类型的数据类型。dattr.valueRank选择UA_VALUERANK_SCALAR,因为结构体里的成员都是单变量。
pointVariableTypeId定义如下,

const UA_NodeId pointVariableTypeId = {
    1, UA_NODEIDTYPE_NUMERIC,{ 4243 } };

特别注意:变量类型的NodeId和数据类型的NodeId是不一样的,都是独立存在的。

然后是给dattr.value赋值,可以设置结构体初始值,最后一个参数传递PointType的地址。
PS:后续使用这个变量类型创建变量时,p会重新生成的,类似于class里的成员变量

最后是使用变量类型创建变量,变量的display名称是"3D Point",browse名称是"3D.Point",

static void
add3DPointVariable(UA_Server *server) {
    Point p;
    p.x = 3.0;
    p.y = 4.0;
    p.z = 5.0;
    UA_VariableAttributes vattr = UA_VariableAttributes_default;
    vattr.description = UA_LOCALIZEDTEXT("en-US", "3D Point");
    vattr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point");
    vattr.dataType = PointType.typeId;
    vattr.valueRank = UA_VALUERANK_SCALAR;
    UA_Variant_setScalar(&vattr.value, &p, &PointType);

    UA_Server_addVariableNode(server, UA_NODEID_STRING(1, "3D.Point"),
        UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
        UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
        UA_QUALIFIEDNAME(1, "3D.Point"),
        pointVariableTypeId, vattr, NULL, NULL);
}

vattr的初始化用于确定数据类型和初始值。UA_Server_addVariableNode()倒数第4个参数是我们添加的变量类型的NodeId,即pointVariableTypeId

4. 使用

首先创建server,并获取其配置,

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

然后把自定义数据类型的信息添加到server的配置里去,本例只有一个数据类型,所以创建只有一个元素的数组,

/* Make your custom datatype known to the stack */
UA_DataType *types = (UA_DataType*)UA_malloc(1 * sizeof(UA_DataType));
UA_DataTypeMember *pointMembers = (UA_DataTypeMember*)UA_malloc(sizeof(UA_DataTypeMember) * 3);
pointMembers[0] = Point_members[0];
pointMembers[1] = Point_members[1];
pointMembers[2] = Point_members[2];
types[0] = PointType;
types[0].members = pointMembers;


/* Attention! Here the custom datatypes are allocated on the stack. So they
* cannot be accessed from parallel (worker) threads. */
UA_DataTypeArray customDataTypes = { config->customDataTypes, 1, types };
config->customDataTypes = &customDataTypes;

UA_DataTypeArray定义如下,

/* Datatype arrays with custom type definitions can be added in a linked list to
 * the client or server configuration. Datatype members can point to types in
 * the same array via the ``memberTypeIndex``. If ``namespaceZero`` is set to
 * true, the member datatype is looked up in the array of builtin datatypes
 * instead. */
typedef struct UA_DataTypeArray {
    const struct UA_DataTypeArray *next;
    const size_t typesSize;
    const UA_DataType *types;
} UA_DataTypeArray;

根据注释,可以知道config->customDataTypes是个单链表,因为目前这个链表只有一个元素,所以next指向了自己。

最后是调用第3步的函数去添加数据类型、变量类型和变量,

add3DPointDataType(server);
add3DPointVariableType(server);
add3DPointVariable(server);
5. 运行

运行后,使用UaExpert去连接,
在这里插入图片描述
可以看到变量已经创建,再看看这个变量的属性,也是和期望的一样,
在这里插入图片描述
添加的数据类型在以下位置,
在这里插入图片描述
添加的变量类型在以下位置,
在这里插入图片描述

6. 整体代码

main.c代码如下,

#include <iostream>
#include <signal.h>
#include <stdlib.h>

#include "open62541.h"
#include "custom_datatype.h"

UA_Boolean running = true;
const UA_NodeId pointVariableTypeId = {
    1, UA_NODEIDTYPE_NUMERIC,{ 4243 } };


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


static void add3DPointDataType(UA_Server* server)
{
    UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point Type");

    UA_Server_addDataTypeNode(
        server, PointType.typeId, UA_NODEID_NUMERIC(0, UA_NS0ID_STRUCTURE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE), UA_QUALIFIEDNAME(1, "3D.Point"), attr, NULL, NULL);
}

static void
add3DPointVariableType(UA_Server *server) {
    UA_VariableTypeAttributes dattr = UA_VariableTypeAttributes_default;
    dattr.description = UA_LOCALIZEDTEXT("en-US", "3D Point");
    dattr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point");
    dattr.dataType = PointType.typeId;
    dattr.valueRank = UA_VALUERANK_SCALAR;

    Point p;
    p.x = 0.0;
    p.y = 0.0;
    p.z = 0.0;
    UA_Variant_setScalar(&dattr.value, &p, &PointType);

    UA_Server_addVariableTypeNode(server, pointVariableTypeId,
        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
        UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
        UA_QUALIFIEDNAME(1, "3D.Point"),
        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
        dattr, NULL, NULL);

}

static void
add3DPointVariable(UA_Server *server) {
    Point p;
    p.x = 3.0;
    p.y = 4.0;
    p.z = 5.0;
    UA_VariableAttributes vattr = UA_VariableAttributes_default;
    vattr.description = UA_LOCALIZEDTEXT("en-US", "3D Point");
    vattr.displayName = UA_LOCALIZEDTEXT("en-US", "3D Point");
    vattr.dataType = PointType.typeId;
    vattr.valueRank = UA_VALUERANK_SCALAR;
    UA_Variant_setScalar(&vattr.value, &p, &PointType);

    UA_Server_addVariableNode(server, UA_NODEID_STRING(1, "3D.Point"),
        UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
        UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
        UA_QUALIFIEDNAME(1, "3D.Point"),
        pointVariableTypeId, vattr, NULL, NULL);
}



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

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

    /* Make your custom datatype known to the stack */
    UA_DataType *types = (UA_DataType*)UA_malloc(1 * sizeof(UA_DataType));
    UA_DataTypeMember *pointMembers = (UA_DataTypeMember*)UA_malloc(sizeof(UA_DataTypeMember) * 3);
    pointMembers[0] = Point_members[0];
    pointMembers[1] = Point_members[1];
    pointMembers[2] = Point_members[2];
    types[0] = PointType;
    types[0].members = pointMembers;


    /* Attention! Here the custom datatypes are allocated on the stack. So they
    * cannot be accessed from parallel (worker) threads. */
    UA_DataTypeArray customDataTypes = { config->customDataTypes, 1, types };
    config->customDataTypes = &customDataTypes;

    add3DPointDataType(server);
    add3DPointVariableType(server);
    add3DPointVariable(server);


    UA_Server_run(server, &running);

    UA_Server_delete(server);
    UA_free(pointMembers);
    UA_free(types);
    return EXIT_SUCCESS;
}

custom_datatype.h如下,

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

typedef struct {
    UA_Float x;
    UA_Float y;
    UA_Float z;
} Point;

/* The datatype description for the Point datatype */
#define Point_padding_y offsetof(Point,y) - offsetof(Point,x) - sizeof(UA_Float)
#define Point_padding_z offsetof(Point,z) - offsetof(Point,y) - sizeof(UA_Float)

/* The binary encoding id's for the datatypes */
#define Point_binary_encoding_id        1


static UA_DataTypeMember Point_members[3] = {
    /* x */
    {
        UA_TYPENAME("x") /* .memberName */
        UA_TYPES_FLOAT,  /* .memberTypeIndex, points into UA_TYPES since namespaceZero is true */
        0,               /* .padding */
        true,            /* .namespaceZero, see .memberTypeIndex */
        false,           /* .isArray */
        false            /* .isOptional */
    },

    /* y */
    {
        UA_TYPENAME("y") /* .memberName */
        UA_TYPES_FLOAT,  /* .memberTypeIndex, points into UA_TYPES since namespaceZero is true */
        Point_padding_y, /* .padding */
        true,            /* .namespaceZero, see .memberTypeIndex */
        false,           /* .isArray */
        false            /* .isOptional */
    },
    /* z */
    {
        UA_TYPENAME("z") /* .memberName */
        UA_TYPES_FLOAT,  /* .memberTypeIndex, points into UA_TYPES since namespaceZero is true */
        Point_padding_z, /* .padding */
        true,            /* .namespaceZero, see .memberTypeIndex */
        false,           /* .isArray */
        false            /* .isOptional */
    }
};

static const UA_DataType PointType = {
        UA_TYPENAME("Point")                /* .tyspeName */
        {1, UA_NODEIDTYPE_NUMERIC, {4242}}, /* .typeId */
        sizeof(Point),                      /* .memSize */
        0,                                  /* .typeIndex, in the array of custom types */
        UA_DATATYPEKIND_STRUCTURE,          /* .typeKind */
        true,                               /* .pointerFree */
        false,                              /* .overlayable (depends on endianness and
                                            the absence of padding) */
        3,                                  /* .membersSize */
        Point_binary_encoding_id,           /* .binaryEncodingId, the numeric
                                            identifier used on the wire (the
                                            namespaceindex is from .typeId) */
        Point_members
};

三 其它例子

其它类型的结构体都是类似,可以参考下example,只要弄懂了一个例子,剩下的都很简单了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐