4. Generating an OPC UA Information Model from XML Descriptions
===============================================================
In the past tutorials you have learned to compile the stack in various configurations, create/delete nodes and manage variables. The core of OPC UA is its data modelling capabilities, and you will invariably find yourself confronted to investigate these relations during runtime. This tutorial will show you how to interact with object and type hierarchies and how to create your own.
Compile XML Namespaces
----------------------
So far we have made due with the hardcoded mini-namespace in the server stack. When writing an application, it is more then likely that you will want to create your own data models using some comfortable GUI based tools like UA Modeler. Most tools can export data to the OPC UA XML specification. open62541 contains a python based namespace compiler that can embed datamodels contained in XML files into the server stack.
Note beforehand that the pyUANamespace compiler you can find in the *tools* subfolder is *not* a XML transformation tool but a compiler. That means that it will create an internal representation (dAST) when parsing the XML files and attempt to understand this representation in order to generate C Code. In consequence, the compiler will refuse to print any inconsistencies or invalid nodes.
As an example, we will create a simple object model using UA Modeler and embed this into the servers nodeset, which is exported to the following XML file:
.. code-block:: xml
http://yourorganisation.org/example_nodeset/i=1i=7i=12i=37i=40i=45i=46i=47i=296providesInputToi=33inputProcidedByFieldDevicei=58ns=1;i=6001ns=1;i=6002ManufacturerNamei=63i=78ns=1;i=1001ModelNamei=63i=78ns=1;i=1001Pumpns=1;i=6003ns=1;i=6004ns=1;i=1001ns=1;i=7001ns=1;i=7002isOni=63i=78ns=1;i=1002MotorRPMi=63i=78ns=1;i=1002startPumpi=78ns=1;i=6005ns=1;i=1002OutputArgumentsi=78ns=1;i=7001i=68i=297startedi=1-1stopPumpi=78ns=1;i=6006ns=1;i=1002OutputArgumentsi=78
ns=1;i=7002
i=68i=297stoppedi=1-1
Or, more consiscly, this::
+------------------+
| <> |
| FieldDevice |
+------------------+
| +------------------+
| | <> |
|------------->| ManufacturerName |
| hasComponent +------------------+
| +------------------+
| | <> |
|------------->| ModelName |
| hasComponent +------------------+
| +----------------+
| | <> |
'------------->| Pump |
hasSubtype +----------------+
|
|
| +------------------+
| | <> |
|--------------->| MotorRPM |
| hasComponent +------------------+
| +------------------+
| | <> |
|--------------->| isOn |
| hasComponent +------------------+
| +------------------+ +------------------+
| | <> | | <> |
|--------------->| startPump |--->| outputArguments |
| hasProperty +------------------+ +------------------+
| +------------------+ +------------------+
| | <> | | <> |
'--------------->| stopPump |--->| outputArguments |
hasProperty +------------------+ +------------------+
UA Modeler prepends the namespace qualifier "uax:" to some fields - this is not supported by the namespace compiler, who has strict aliasing rules concerning field names. If a datatype defines a field called "Argument", the compiler expects to find "" tags, not "". Remove/Substitute such fields to remove namespace qualifiers.
The namespace compiler can be invoked manually and has numerous options. In its simplest form, an invokation will look like this::
python ./generate_open62541CCode.py ../schema/namespace0/Opc.Ua.NodeSet2.xml ///.xml ///.xml myNamespace
The above call first parses Namespace 0, which provides all dataTypes, referenceTypes, etc.. An arbitrary amount of further xml files can be passed as options, whose nodes will be added to the abstract syntax tree. The script will then create the files ``myNamespace.c`` and ``myNamespace.h`` containing the C code necessary to instantiate those namespaces.
Although it is possible to run the compiler this way, it is highly discouraged. If you care to examine the CMakeLists.txt (toplevel directory), you will find that compiling the stack with ``DENABLE_GENERATE_NAMESPACE0`` will execute the following command::
COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/tools/pyUANamespace/generate_open62541CCode.py
-i ${PROJECT_SOURCE_DIR}/tools/pyUANamespace/NodeID_AssumeExternal.txt
-s description -b ${PROJECT_SOURCE_DIR}/tools/pyUANamespace/NodeID_Blacklist.txt
${PROJECT_SOURCE_DIR}/tools/schema/namespace0/${GENERATE_NAMESPACE0_FILE}
${PROJECT_BINARY_DIR}/src_generated/ua_namespaceinit_generated
Albeit a bit more complicated then the previous description, you can see that a the namespace 0 XML file is loaded in the line before the last, and that the output will be in ``ua_namespaceinit_generated.c/h``. In order to take advantage of the namespace compiler, we will simply append our nodeset to this call and have cmake care for the rest. Modify the CMakeLists.txt line above to contain the relative path to your own XML file like this::
COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/tools/pyUANamespace/generate_open62541CCode.py
-i ${PROJECT_SOURCE_DIR}/tools/pyUANamespace/NodeID_AssumeExternal.txt
-s description -b ${PROJECT_SOURCE_DIR}/tools/pyUANamespace/NodeID_Blacklist.txt
${PROJECT_SOURCE_DIR}/tools/schema/namespace0/${GENERATE_NAMESPACE0_FILE}
${PROJECT_SOURCE_DIR}/////.xml
${PROJECT_BINARY_DIR}/src_generated/ua_namespaceinit_generated
Always make sure that your XML file comes *after* namespace 0. Also, take into consideration that any node ID's you specify that already exist in previous files will overwrite the previous file (yes, you could intentionally overwrite the NS0 Server node if you wanted to). The namespace compiler will now automatically embedd you namespace definitions into the namespace of the server. So in total, all that was necessary was:
* Creating your namespace XML description
* Adding the relative path to the file into CMakeLists.txt
* Compiling the stack
After adding your XML file to CMakeLists.txt, rerun cmake in your build directory and enable ``DENABLE_GENERATE_NAMESPACE0``. Make especially sure that you are using the option ``CMAKE_BUILD_TYPE=Debug``. The generated namespace contains more than 30000 lines of code and many strings. Optimizing this amount of code with -O2 or -Os options will require several hours on most PCs! Also make sure to enable ``-DENABLE_METHODCALLS``, as namespace 0 does contain methods that need to be encoded::
ichrispa@Cassandra:open62541/build> cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_METHODCALLS=On -BUILD_EXAMPLECLIENT=On -BUILD_EXAMPLESERVER=On -DENABLE_GENERATE_NAMESPACE0=On ../
-- Git version: v0.1.0-RC4-403-g198597c-dirty
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ichrispa/work/svn/working_copies/open62541/build
ichrispa@Cassandra:open62541/build> make
[ 3%] Generating src_generated/ua_nodeids.h
[ 6%] Generating src_generated/ua_types_generated.c, src_generated/ua_types_generated.h
[ 10%] Generating src_generated/ua_transport_generated.c, src_generated/ua_transport_generated.h
[ 13%] Generating src_generated/ua_namespaceinit_generated.c, src_generated/ua_namespaceinit_generated.h
At this point, the make process will most likely hang for 30-60s until the namespace is parsed, checked, linked and finally generated (be patient). It should continue as follows::
Scanning dependencies of target open62541-object
[ 17%] Building C object CMakeFiles/open62541-object.dir/src/ua_types.c.o
[ 20%] Building C object CMakeFiles/open62541-object.dir/src/ua_types_encoding_binary.c.o
[ 24%] Building C object CMakeFiles/open62541-object.dir/src_generated/ua_types_generated.c.o
[ 27%] Building C object CMakeFiles/open62541-object.dir/src_generated/ua_transport_generated.c.o
[ 31%] Building C object CMakeFiles/open62541-object.dir/src/ua_connection.c.o
[ 34%] Building C object CMakeFiles/open62541-object.dir/src/ua_securechannel.c.o
[ 37%] Building C object CMakeFiles/open62541-object.dir/src/ua_session.c.o
[ 41%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_server.c.o
[ 44%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_server_addressspace.c.o
[ 48%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_server_binary.c.o
[ 51%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_nodes.c.o
[ 55%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_server_worker.c.o
[ 58%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_securechannel_manager.c.o
[ 62%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_session_manager.c.o
[ 65%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_discovery.c.o
[ 68%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_securechannel.c.o
[ 72%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_session.c.o
[ 75%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_attribute.c.o
[ 79%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_nodemanagement.c.o
[ 82%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_view.c.o
[ 86%] Building C object CMakeFiles/open62541-object.dir/src/client/ua_client.c.o
[ 89%] Building C object CMakeFiles/open62541-object.dir/examples/networklayer_tcp.c.o
[ 93%] Building C object CMakeFiles/open62541-object.dir/examples/logger_stdout.c.o
[ 96%] Building C object CMakeFiles/open62541-object.dir/src_generated/ua_namespaceinit_generated.c.o
And at this point, you are going to see the compiler hanging again. If you specified ``-DCMAKE_BUILD_TYPE=Debug``, you are looking at about 5-10 seconds of waiting. If you forgot, you can now drink a cup of coffee, go to the movies or take a loved one out for dinner (or abort the build with CTRL+C). Shortly after::
[ 83%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_services_call.c.o
[ 86%] Building C object CMakeFiles/open62541-object.dir/src/server/ua_nodestore.c.o
[100%] Built target open62541-object
Scanning dependencies of target open62541
Linking C shared library libopen62541.so
[100%] Built target open62541
If you open the header ``src_generated/ua_namespaceinit_generated.h`` and take a short look at the generated defines, you will notice the following definitions have been created:
.. code-block:: c
#define UA_NS1ID_PROVIDESINPUTTO
#define UA_NS1ID_FIELDDEVICE
#define UA_NS1ID_PUMP
#define UA_NS1ID_STARTPUMP
#define UA_NS1ID_STOPPUMP
These definitions are generated for all types, but not variables, objects or views (as their names may be ambiguous and may not a be unique identifier). You can use these definitions in your code as you already used the ``UA_NS0ID_`` equivalents.
Now switch back to your own source directory and update your libopen62541 library (in case you have not linked it into the build folder). Compile our example server as follows::
ichrispa@Cassandra:open62541/build-tutorials> gcc -g -std=c99 -Wl,-rpath,`pwd` -I ./include -L . -DENABLE_METHODCALLS -o server ./server.c -lopen62541
Note that we need to also define the method-calls here, as the header files may choose to ommit functions such as UA_Server_addMethodNode() if they believe you do not use them. If you run the server, you should now see a new dataType in the browse path ``/Types/ObjectTypes/BaseObjectType/FieldDevice`` when viewing the nodes in UAExpert.
If you take a look at any of the variables, like ``ManufacturerName``, you will notice it is shown as a Boolean; this is not an error. The node does not include a variant and as you learned in our previous tutorial, it is that variant that would hold the dataType ID.
A minor list of some of the miriad things that can go wrong:
* Your file was not found. The namespace compiler will complain, print a help message, and exit.
* A structure/DataType you created with a value was not encoded. The namespace compiler can currently not handle nested extensionObjects.
* Nodes are not or wrongly encoded or you get nodeId errors. The namespace compiler can currently not encode bytestring or guid node id's and external server uris are not supported either.
* You get compiler complaints for non-existant variants. Check that you have removed any namespace qualifiers (like "uax:") from the XML file.
* You get "invalid reference to addMethodNode" style errors. Make sure ``-DDENABLE_METHODCALLS=On`` is defined.
Creating object instances
-------------------------
Defining an object type is only usefull if it ends up making our lives easier in some way (though it is always the proper thing to do). One of the key benefits of defining object types is being able to create object instances fairly easily. Object instantiation is handled automatically when the typedefinition NodeId points to a valid ObjectType node. All Attributes and Methods contained in the objectType definition will be instantiated along with the object node.
While variables are copied from the objetType definition (allowing the user for example to attach new dataSources to them), methods are always only linked. This paradigm is identical to languages like C++: The method called is always the same piece of code, but the first argument is a pointer to an object. Likewise, in OPC UA, only one methodCallback can be attached to a specific methodNode. If that methodNode is called, the parent objectId will be passed to the method - it is the methods job to derefence which object instance it belongs to in that moment.
One of the problems arising from the server internally "building" new nodes as described in the type is that the user does not know which template creates which instance. This can be a problem - for example if a specific dataSource should be attached to each variableNode called "samples" later on. Unfortunately, we only know which template variable's Id the dataSource will be attached to - we do not know the nodeId of the instance of that variable. To easily cover usecases where variable instances Y derived from a definition template X should need to be manipulated in some maner, the stack provides an instantiation callback: Each time a new node is instantiated, the callback gets notified about the relevant data; the callback can then either manipulate the new node itself or just create a map/record for later use.
Let's look at an example that will create a pump instance given the newly defined objectType:
.. code-block:: c
#include
#include
#include "ua_types.h"
#include "ua_server.h"
#include "ua_namespaceinit_generated.h"
#include "logger_stdout.h"
#include "networklayer_tcp.h"
UA_Boolean running;
UA_Int32 global_accessCounter = 0;
void stopHandler(int signal) {
running = 0;
}
static UA_StatusCode pumpInstantiationCallback(UA_NodeId objectId, UA_NodeId definitionId,
void *handle) {
printf("Created new node ns=%d;i=%d according to template ns=%d;i=%d (handle was %d)\n",
objectId.namespaceIndex, objectId.identifier.numeric,
definitionId.namespaceIndex, definitionId.identifier.numeric, *((UA_Int32 *) handle));
return UA_STATUSCODE_GOOD;
}
int main(void) {
signal(SIGINT, stopHandler);
signal(SIGTERM, stopHandler);
UA_Server *server = UA_Server_new(UA_ServerConfig_standard);
UA_Server_addNetworkLayer(server, ServerNetworkLayerTCP_new(UA_ConnectionConfig_standard, 16664));
running = true;
UA_NodeId createdNodeId;
UA_Int32 myHandle = 42;
UA_ObjectAttributes object_attr;
UA_ObjectAttributes_init(&object_attr);
object_attr.description = UA_LOCALIZEDTEXT("en_US","A pump!");
object_attr.displayName = UA_LOCALIZEDTEXT("en_US","Pump1");
UA_InstantiationCallback theAnswerCallback = {.method=pumpInstantiationCallback, .handle=(void*) &myHandle};
UA_Server_addObjectNode(server, UA_NODEID_NUMERIC(1, DEMOID),
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES), UA_QUALIFIEDNAME(1, "Pump1"),
UA_NODEID_NUMERIC(0, UA_NS1ID_PUMPTYPE), object_attr, theAnswerCallback, &createdNodeId);
UA_Server_run(server, 1, &running);
UA_Server_delete(server);
printf("Bye\n");
return 0;
}
Make sure you have updated the headers and libs in your project, then recompile and run the server. Make especially sure you have added ``ua_namespaceinit_generated.h`` to your include folder and that you have removed any references to header in ``server``. The only include you are going to need is ``ua_types.h``.
As you can see instantiating an object is not much different from creating an object node. The main difference is that you *must* use an objectType node as typeDefinition and you (may) pass a callback function (``pumpInstantiationCallback``) and a handle (``myHandle``). You should already be familiar with callbacks and handles from our previous tutorial and you can easily derive how the callback is used by running the server binary, which produces the following output::
Created new node ns=1;i=1505 according to template ns=1;i=6001 (handle was 42)
Created new node ns=1;i=1506 according to template ns=1;i=6002 (handle was 42)
Created new node ns=1;i=1507 according to template ns=1;i=6003 (handle was 42)
Created new node ns=1;i=1508 according to template ns=1;i=6004 (handle was 42)
Created new node ns=1;i=1510 according to template ns=1;i=6001 (handle was 42)
Created new node ns=1;i=1511 according to template ns=1;i=6002 (handle was 42)
Created new node ns=1;i=1512 according to template ns=1;i=6003 (handle was 42)
Created new node ns=1;i=1513 according to template ns=1;i=6004 (handle was 42)
If you start the server and inspect the nodes with UA Expert, you will find two pumps in the objects folder, which look like this::
+------------+
| <