1
votes

As am starting out with opc ua , i just wanted to know what happens under the hood of the communication layer of opc ua.

Lets take an example of a very simple server implementation that has 3 nodes in the address space. These nodes present data that can be written and read by an opc-UA client.

From reading a part of the code that comes with open62541, i've learnt that the communication happen via TCP. Meaning the server initiates a socket that a client can connect to and enable the client to perform various operations on the nodes.

My question is, how does the client know about available server nodes ? I know it browses through the address space but where exactly does it browse to find available nodes ? what exposure mechanism does opc-UA use to present available nodes to the client ?. Does the server write available information & nodes on a some xml file or anywhere else and hence when a clients connects, it tries to read the contents of the file to understand the addressSpace structure ?

Sample server implementation for 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;
}
3
The OPC UA Client shall use the Services FindServers and GetEndpoints. I will recommend you first to get some information about OPC UA. There is a list of books for OPC UA here : opcfoundation.org/resources/booksCamille G.

3 Answers

1
votes

There are a couple of ways you can discover Nodes.

First and foremost, you should be aware that the AddressSpace is not a tree, but a graph. The nodes of the graph are the OPC UA Nodes, and the edges of the graph are OPC UA References.

The name of a Node is a NodeId which is a qualified name. The name is either an integer (i=), a string (s=), or an opaque object (o=). The qualifier designates a Namespace in the server's namespace table.

About namespaces, there are two reserved namespace indices:

  • 0, which designates the OPC UA namespace
  • 1, which designated the server itself (IMHO, you should have one different per server)

The OPC UA Foundation is master of the OPC UA namespace, and defines a whole bunch of standard nodes in the 0 namespace. In particular the nodes Server, Objects and Types are defined in the 0 namespace, with well-known integer names. I won't talk about the Attributes of a Node, but given the concepts of Node, Reference, Namespace and Attribute, the OPC UA Standard pulls itself by the bootstraps. The 0 namespace defines the base structure of the nodes of a server, and all defined nodes have well-known NodeIds. I said "bootstrap", because in paricular under the Server node is located the NamespaceTable which associates the namespace indices to the corresponding namespace URNs. (including the standardized indices 0 and 1) The elements of the table can be Read just like any other node.

To begin answering your question, in short, the most direct way of accessing a particular node within a server is to know its NodeId.

Now, how can you know what nodes are present on a server, if you don't have a list? Well, the operation is known as Browsing, and there are two flavours about it: following References, or teleporting through BrowsePaths.

About following references, remember I said that the AddressSpace is a graph. Well, a Node points to other Nodes through References. Given a particular NodeId (e.g. a well known NodeId, or the root, which also has a well-known NodeId in the namespace 0), you can query the references of the Node which will designate other Nodes, and follow the trail that is of interest to you from Nodeto Node, until you find what you are looking for. This implies lots of exchanges between the client and the server, and honestly is seldomly worth the hassle.

About teleporting through BrowsePaths, there is a service called TranslateBrowsePath implemented by the server where given a start NodeIdand a browse path, the server gives you the list of nodes that match the query. (i.e. reachable from the designated start Node through a path of References matching the browse path) The browse path language is very rich, and you can make rather complex queries with it.

0
votes

The client can discover nodes in the server using the Browse service.

There are some pre-defined Nodes present in every server that the client can start the browse from. Typically that would be either the Root folder or the Objects folder.

0
votes

The nodes are identified by a structure of type NodeId.

If you program an OPC UA client you would have to add the functionality of navigate through the node's tree so the user can choose the NodeId(s) he wants to read or write their attributes, among them the value.

To navigate through the tree you need to use the Browse service, to read the attributes you need the Read service. Browse returns the children of a given node.

But to use these services you first have to create a session, for which you have to call first GetEndpoints, OpenSecureChannel, CreateSession, ActivateSession .... services