1

当我开始使用 opc ua 时,我只是想知道在 opc ua 通信层的底层发生了什么。

让我们举一个非常简单的服务器实现的例子,它在地址空间中有 3 个节点。这些节点提供可由 opc-UA 客户端写入和读取的数据。

通过阅读 open62541 附带的部分代码,我了解到通信是通过 TCP 发生的。这意味着服务器启动一个客户端可以连接的套接字,并使客户端能够在节点上执行各种操作。

我的问题是,客户端如何知道可用的服务器节点?我知道它浏览地址空间,但它究竟在哪里浏览以查找可用节点?opc-UA 使用什么暴露机制向客户端呈现可用节点?服务器是否在某个 xml 文件或其他任何地方写入可用信息和节点,因此当客户端连接时,它会尝试读取文件的内容以了解 addressSpace 结构?

open62541 的示例服务器实现

#include <stdio.h>
#include <open62541.h>
#include <signal.h>

static void
addVariable(UA_Server *server) {
    /* Define the attribute of the myInteger variable node */
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    UA_Int32 myInteger = 43;
    UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
    attr.description = UA_LOCALIZEDTEXT("en-US", "the answer");
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "the answer");
    attr.dataType = UA_TYPES[UA_TYPES_INT32].typeId;
    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;

    /* Add the variable node to the information model */
    UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "the.answer");
    UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, "the answer");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_Server_addVariableNode(server, myIntegerNodeId, parentNodeId,
        parentReferenceNodeId, myIntegerName,
        UA_NODEID_NULL, attr, NULL, NULL);
}

static void
addThirdVariable(UA_Server *server) {
    /* Define the attribute of the myInteger variable node */
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    UA_String myInteger = UA_STRING("My name is variable 3"); // variable name
    UA_Variant_setScalar(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_STRING]);
    attr.description = UA_LOCALIZEDTEXT("en-US", "the answer");
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "the answer");
    attr.dataType = UA_TYPES[UA_TYPES_STRING].typeId;
    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;

    /* Add the variable node to the information model */
    UA_NodeId myIntegerNodeId = UA_NODEID_STRING(1, "third.variable");
    UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME(1, "third varaible");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_Server_addVariableNode(server, myIntegerNodeId, parentNodeId,
        parentReferenceNodeId, myIntegerName,
        UA_NODEID_NULL, attr, NULL, NULL);
}

void addSecondVariable(UA_Server * server) {
    //variable attributes
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    UA_String machine_name = UA_STRING("My name is a machine"); // variable name
    UA_Variant_setScalar(&attr.value, &machine_name, &UA_TYPES[UA_TYPES_STRING]);

    attr.description = UA_LOCALIZEDTEXT("en-US", "machine name");
    attr.displayName = UA_LOCALIZEDTEXT("en-US", "machine name");
    attr.dataType = UA_TYPES[UA_TYPES_STRING].typeId;
    //setting access level not important

    //add the variable to the information model
    UA_NodeId myStringNodeID = UA_NODEID_STRING(1, "the.machine");
    UA_QualifiedName myStringName = UA_QUALIFIEDNAME(1, "the machine");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);

    UA_Server_addVariableNode(server, myStringNodeID, parentNodeId,
        parentReferenceNodeId, myStringName,
        UA_NODEID_NULL, attr, NULL, NULL);



}

UA_Boolean running = true;
static void stopHandler(int sign) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}

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

    UA_ServerConfig *config = UA_ServerConfig_new_default();
    UA_Server *server = UA_Server_new(config);
    addVariable(server);
    addSecondVariable(server);
    addThirdVariable(server);
    UA_StatusCode retval = UA_Server_run(server, &running);


    UA_Server_delete(server);
    UA_ServerConfig_delete(config);
    return (int)retval;
}
4

3 回答 3

1

有几种方法可以发现节点。

首先,您应该知道 AddressSpace 不是树,而是图。图的节点是 OPC UA Node,图的边是 OPC UA Reference

a 的名称NodeNodeId一个限定名称。名称可以是整数 ( i=)、字符串 ( s=) 或不透明对象 ( o=)。限定符Namespace在服务器的命名空间表中指定 a。

关于命名空间,有两个保留的命名空间索引:

  • 0,它指定 OPC UA 命名空间
  • 1,它指定了服务器本身(恕我直言,每台服务器应该有一个不同的)

OPC UA 基金会是 OPC UA 命名空间的主人,并在命名空间中定义了一大堆标准节点0。特别是节点Server和在命名空间ObjectsTypes定义0,具有众所周知的整数名称。我不会谈论Attributea 的 s Node,但考虑到、和的概念Node,OPC UA 标准会自行引导。命名空间定义了服务器节点的基本结构,所有定义的节点都有着名的s。我说的是“引导程序”,因为特别是在节点下方,它将命名空间索引与相应的命名空间 URN 相关联。(包括标准化指数和ReferenceNamespaceAttribute0NodeIdServerNamespaceTable01) 表的元素可以Read像任何其他节点一样。

要开始回答您的问题,简而言之,访问服务器中特定节点的最直接方法是了解其NodeId.

现在,如果您没有列表,您如何知道服务器上存在哪些节点?好吧,这个操作被称为Browsing,它有两种风格:跟随References,或通过 s 传送BrowsePath

关于以下参考资料,请记住我说的AddressSpace是图表。嗯,a通过sNode指向其他s。给定一个特定的(例如,一个 well known或根,它在 namespace中也有一个 well-known ),您可以查询将指定其他s 的引用,并按照您感兴趣的线索从到, 直到你找到你要找的东西。这意味着客户端和服务器之间的大量交换,老实说很少值得麻烦。NodeReferenceNodeIdNodeIdNodeId0NodeNodeNodeNode

关于通过 s 传送BrowsePath,有一个TranslateBrowsePath由服务器实现的服务,在给定开始NodeId和浏览路径的情况下,服务器会为您提供与查询匹配的节点列表。Node(即通过与浏览路径匹配的路径从指定起点到达Reference) 浏览路径语言非常丰富,可以用它进行比较复杂的查询。

于 2019-01-02T14:54:35.543 回答
0

客户端可以使用浏览服务发现服务器中的节点。

每个服务器中都有一些预定义的节点,客户端可以从中开始浏览。通常,这将是根文件夹或对象文件夹。

于 2017-12-19T16:27:41.720 回答
0

节点由NodeId类型的结构标识。

如果您对 OPC UA 客户端进行编程,则必须添加浏览节点树的功能,以便用户可以选择他想要读取或写入其属性的NodeId ,其中包括值。

要在树中导航,您需要使用Browse服务,来读取您需要Read服务的属性。Browse 返回给定节点的子节点。

但是要使用这些服务,您首先必须创建一个会话,为此您必须首先调用 GetEndpoints、OpenSecureChannel、CreateSession、ActivateSession .... 服务

于 2017-12-19T17:16:37.803 回答