学习open62541 --- [14] 路径搜索

本文主要讲述open62541里的路径搜索,这功能具体是干啥的呢?用过UaExpert的都看过如下这个界面,这是OPC UA Server的地址空间
在这里插入图片描述
可以看出OPC UA Server的地址空间是用层级目录组织的,英文叫hierarchy,根节点是Root。每个节点都会有个路径,如Server的路径就是/Root/Objects/Server。而open62541的路径搜索就是通过路径去获取目标节点的NodeId,下面就讲述下如何操作。


一 OPC UA Server端

先说Server端如何使用路径搜索,后面讲Client端。

1. 添加对象节点Student

使用以下代码添加一个叫Student的对象节点,这个节点有2个Component:姓名(Name)和性别(Gender)

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");
    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"), 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_Server_addObjectNode()和UA_Server_addVariableNode()时,第2个参数都是给的UA_NODEID_NULL,表示这些节点的Id由Server去分配,这样我们预先就不知道它们的Id了,只有在运行时才知道。

2. 路径搜索函数

这个函数是从open62541源码里拷贝过来的,做了一些修改,

static int findChildId(UA_Server *server, 
                        UA_NodeId parentNode, 
                        UA_NodeId referenceType,
                        const UA_QualifiedName targetName, 
                        UA_NodeId *result) 
{
    int ret = 0;
    UA_RelativePathElement rpe;
    UA_RelativePathElement_init(&rpe);
    rpe.referenceTypeId = referenceType;
    rpe.isInverse = false;
    rpe.includeSubtypes = false;
    rpe.targetName = targetName;

    UA_BrowsePath bp;
    UA_BrowsePath_init(&bp);
    bp.startingNode = parentNode;
    bp.relativePath.elementsSize = 1;
    bp.relativePath.elements = &rpe;

    UA_BrowsePathResult bpr = UA_Server_translateBrowsePathToNodeIds(server, &bp);
    if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1)
    {
        printf("error: %s, targetsSize: %d\n", 
                UA_StatusCode_name(bpr.statusCode), bpr.targetsSize);
        ret = -1;
    }
    else
    {
        UA_NodeId_copy(&bpr.targets[0].targetId.nodeId, result);
    }

    
    UA_BrowsePathResult_deleteMembers(&bpr);

    return ret;
}

首先解释这个函数的参数

  • server:OPC UA Server的指针
  • parentNode:路径搜索的start节点的NodeId
  • referenceType:start节点和下一个子节点之间的reference类型
  • targetName:目标节点的qualified名称
  • result:存放目标节点NodeId的指针

代码解释:

  • UA_RelativePathElement类型用来描述路径中的单个位置节点,其定义如下,
    在这里插入图片描述
    referenceTypeId用来表示本节点和父节点之间的reference关系
    targetName就是本节点的qualified名称
  • UA_BrowsePath类型用来描述整个搜索路径
    在这里插入图片描述
    在这里插入图片描述
    UA_BrowsePath的startingNode表示整个搜索路径得start节点,relativePath则是一个数组,用来存放剩余路径信息,假如整体路径是A/B/C,那么startingNode就是A,B和C则要按顺序存放到relativePath的elements里,并把elementsSize设置为2
  • UA_Server_translateBrowsePathToNodeIds()把路径转为NodeId,存放在UA_BrowsePathResult 类型的对象里,整个类型大家看下源码就行了
3. 执行搜索

这是main函数,搜索路径是Objects/Student,起点是Objects,目标节点是Student,也就是前面添加的对象节点,Objects和Student的reference关系是organize,可能会问:我怎么知道它们之间的关系?在添加Student时调用了UA_Server_addObjectNode(),其第4个参数就是organize

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

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));
    
    manuallyDefineStudent(server);


    int ret = 0;
    UA_NodeId returnId;
    ret = findChildId(server, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), 
                        UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), 
                        UA_QUALIFIEDNAME(1, "Student (Manual)"), &returnId);

    if (ret == 0)
    {
        if (returnId.identifierType == UA_NODEIDTYPE_NUMERIC)
        {
            printf("==> return Id: %u\n", returnId.identifier.numeric);
        }
    }


    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

运行结果如下,
在这里插入图片描述
我们使用UaExpert连接再确认下,如下,
在这里插入图片描述
可以看出是对的。

4. 优化

上面的路径搜索函数findChildId()有个缺点,就是只能搜索一层,无法搜索多层,也就是说只能搜索A/B这种,不能搜索A/B/C或者更多层路径节点。

因为前面已经讲述了相关结构体的含义,所以这里可以做个优化,让函数可以搜索更多层,代码如下,

static int findChildId2(UA_Server *server, 
                        UA_NodeId parentNode, 
                        const int relativePathCnt, 
                        const UA_NodeId referenceTypeArr[],
                        const UA_QualifiedName targetNameArr[], 
                        UA_NodeId *result) 
{
    int ret = 0;
    
    UA_RelativePathElement rpe[relativePathCnt];
    for (int i = 0; i < relativePathCnt; ++i)
    {
        UA_RelativePathElement_init(&rpe[i]);
        rpe[i].referenceTypeId = referenceTypeArr[i];
        rpe[i].isInverse = false;
        rpe[i].includeSubtypes = false;
        rpe[i].targetName = targetNameArr[i];
    }

    UA_BrowsePath bp;
    UA_BrowsePath_init(&bp);
    bp.startingNode = parentNode;
    bp.relativePath.elementsSize = relativePathCnt;
    bp.relativePath.elements = rpe;


    UA_BrowsePathResult bpr = UA_Server_translateBrowsePathToNodeIds(server, &bp);
    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;

}

参数介绍:

  • server:OPC UA Server的指针
  • parentNode:路径搜索的start节点的NodeId
  • relativePathCnt:除start节点外,相对路径中的元素个数
  • referenceTypeArr[]:reference关系数组
  • targetNameArr[]:除start节点外,相对路径中的元素qualified名称数组
  • result:存放目标节点NodeId的指针

使用如下,

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

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));
    
    manuallyDefineStudent(server);


    int ret = 0;

    UA_NodeId newReturnId;
    UA_NodeId referenceTypeArr[2] = {UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT)};
    UA_QualifiedName targetNameArr[2] = {UA_QUALIFIEDNAME(1, "Student"), UA_QUALIFIEDNAME(1, "StudentName")};  
    ret = findChildId2(server, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER), 
                2, referenceTypeArr, targetNameArr, &newReturnId);

    if (ret == 0)
    {
        if (returnId.identifierType == UA_NODEIDTYPE_NUMERIC)
        {
            printf("==> newReturn Id: %u\n", newReturnId.identifier.numeric);
        }
    }


    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

搜索路径是Objects/Student/StudentName,Objects和Student之间关系是organize,Student和StudentName之间的关系是hascomponent。

另外路径中元素的名称都是qualified名称,不是display 名称,例如,这里的StudentName对应的节点,其display name是“Name”

运行如下,
在这里插入图片描述
使用UaExpert验证ok,如下,
在这里插入图片描述

5. 更加简便的方法

上面的方法虽然原理不难,但是用起来比较麻烦,需要定义一堆变量并设置,需要知道很多的细节,而open62541提供了一个更加简单的api,就是UA_Server_browseSimplifiedBrowsePath(),其原型如下,

UA_BrowsePathResult
UA_Server_browseSimplifiedBrowsePath(UA_Server *server, const UA_NodeId origin,
                                     size_t browsePathSize, const UA_QualifiedName *browsePath)

只要确定start节点,和剩余路径元素的qualified名称,就可以去搜索了,这也是利用了相同层级下不会有重名节点这个原理。下面是对其进行封装的简单函数,

static int findChildId3(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;
}

main函数如下,

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_QualifiedName targetNameArr[2] = {UA_QUALIFIEDNAME(1, "Student"), UA_QUALIFIEDNAME(1, "StudentName")};

    UA_NodeId newReturnId2;
    int ret = findChildId3(server, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
            2, targetNameArr, &newReturnId2);

    if (ret == 0)
    {
        if (newReturnId2.identifierType == UA_NODEIDTYPE_NUMERIC)
        {
            printf("==> newReturn Id: %u\n", newReturnId2.identifier.numeric);
        }
    }

    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

运行结果如下,
在这里插入图片描述
同样,UaExpert验证ok,
在这里插入图片描述


二 OPC UA Client端

client端只有一个api可以用,就是UA_Client_Service_translateBrowsePathsToNodeIds(),其原型如下,

static UA_INLINE UA_TranslateBrowsePathsToNodeIdsResponse
UA_Client_Service_translateBrowsePathsToNodeIds(UA_Client *client,
                        const UA_TranslateBrowsePathsToNodeIdsRequest request)

下面是个简陋的封装函数,

static UA_StatusCode translateBrowsePathsToNodeIdsRequest(UA_Client *client, UA_NodeId *returnId) 
{
    UA_StatusCode ret = UA_STATUSCODE_GOOD;
    
    #define BROWSE_PATHS_SIZE 3
    char *paths[BROWSE_PATHS_SIZE] = {"Objects", "Student", "StudentName"};
    UA_UInt32 ids[BROWSE_PATHS_SIZE] = {UA_NS0ID_ORGANIZES, UA_NS0ID_ORGANIZES, UA_NS0ID_HASCOMPONENT};
    int nsNumOfQualifiedName[BROWSE_PATHS_SIZE] = {0, 1, 1}; // namespace number of qualified name

    UA_BrowsePath browsePath;
    UA_BrowsePath_init(&browsePath);
    browsePath.startingNode = UA_NODEID_NUMERIC(0, UA_NS0ID_ROOTFOLDER); // start节点是Root
    browsePath.relativePath.elements = (UA_RelativePathElement*)UA_Array_new(BROWSE_PATHS_SIZE, &UA_TYPES[UA_TYPES_RELATIVEPATHELEMENT]);
    browsePath.relativePath.elementsSize = BROWSE_PATHS_SIZE;

    for(size_t i = 0; i < BROWSE_PATHS_SIZE; ++i) {
        UA_RelativePathElement *elem = &browsePath.relativePath.elements[i];
        elem->referenceTypeId = UA_NODEID_NUMERIC(0, ids[i]);
        elem->targetName = UA_QUALIFIEDNAME_ALLOC(nsNumOfQualifiedName[i], paths[i]);
    }

    UA_TranslateBrowsePathsToNodeIdsRequest request;
    UA_TranslateBrowsePathsToNodeIdsRequest_init(&request);
    request.browsePaths = &browsePath;
    request.browsePathsSize = 1;

    UA_TranslateBrowsePathsToNodeIdsResponse response = UA_Client_Service_translateBrowsePathsToNodeIds(client, request);
    if (response.responseHeader.serviceResult == UA_STATUSCODE_GOOD)
    {
        if (response.resultsSize == 1 && response.results[0].targetsSize == 1)
        {
            UA_NodeId_copy(&response.results[0].targets[0].targetId.nodeId, returnId);
        }
    }
    else
    {
        printf("Error: %s\n", UA_StatusCode_name(response.responseHeader.serviceResult));
        ret = response.responseHeader.serviceResult;
    }

    UA_BrowsePath_deleteMembers(&browsePath);
    UA_TranslateBrowsePathsToNodeIdsResponse_deleteMembers(&response);

    return ret;
}

代码中使用了hardcode,只是为了便捷一点,这里的搜索路径是Root/Objects/Student/StudentName,从根节点开始的。
代码解释

  • Client端也同样用到了UA_BrowsePath和UA_RelativePathElement这2个结构体
  • UA_TranslateBrowsePathsToNodeIdsRequest把路径打包进去,形成一个request
  • 调用UA_Client_Service_translateBrowsePathsToNodeIds()处理这个请求
  • 返回信息存放在UA_TranslateBrowsePathsToNodeIdsResponse

说实在的,这些结构体名字都很长,不过里面的结构元素不是很难分析,看看源码或者本文代码就够用了。

main函数如下,

int main(int argc, char *argv[]) 
{
    UA_Client *client = UA_Client_new();
    UA_ClientConfig_setDefault(UA_Client_getConfig(client));

    // Connect to OPC UA server
    UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
    if(retval != UA_STATUSCODE_GOOD) 
    {
        UA_Client_delete(client);
        return EXIT_FAILURE;
    }

    UA_NodeId targetId;
    retval = translateBrowsePathsToNodeIdsRequest(client, &targetId);
    if (retval == UA_STATUSCODE_GOOD)
    {
        if (targetId.identifierType == UA_NODEIDTYPE_NUMERIC)
        {
            printf("==> target Id: %u\n", targetId.identifier.numeric);
        }
    }

    UA_Client_disconnect(client);
    UA_Client_delete(client);

    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

运行结果:
在这里插入图片描述
同样,UaExpert验证ok,
在这里插入图片描述


三 总结

本文主要讲述如何在OPC UA里进行路径搜索,以获取目标节点的NodeId。
这里简单说下使用场景:

  1. 当节点很多时,这个方法的优势就明显了,就像你电脑里文件太多了,要找某个文件时,使用搜索是最便捷的
  2. 还有一种情形,假设工程里已经定义了对象类型(可以看这篇文章),用户只能使用这个类型去创建对象,那么这个对象下所拥有的其它节点的NodeId就是随机分配的,这时想知道它们的id,就只能使用路径搜索

如果有写的不对的地方,希望能留言指正,谢谢阅读。

评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值