XML Nodeset Compiler

When writing an application, it is more comfortable to create information models using GUI tools. Most tools can export data according the OPC UA Nodeset XML schema. open62541 contains a Python based nodeset compiler that transforms Nodeset XML files into code for the open62541 SDK.

Custom DataTypes

When a UADataType contains a NodeSet2 Definition element, the compiler generates the matching C enum, structure, optional structure, or union in a static UA_TYPES_<NAME> array. The generated loader takes a UA_Boolean copy_types argument, for example namespace_example_generated(server, true). The DataTypeDefinition attribute is reconstructed from the registered layout when it is read. Definitions can refer to custom datatypes declared later in the same NodeSet2 file; the compiler resolves those dependencies before code generation. Inheritance is expanded once by the NodeSet input adapter; the existing C datatype backend is shared with BSD input. Structure members must be scalar or one-dimensional arrays. Matrix and variable-rank fields produce a warning: their native descriptors, and native layouts that depend on them, are omitted because the current datatype layout cannot represent their encoding. The namespace nodes are still generated. AllowSubTypes fields use ExtensionObjects for structures and Variants for other types, as in the runtime loader. OptionSets retain their integer base width or, for subtypes of the standard OptionSet, the two ByteStrings Value and ValidBits. Simple aliases retain the builtin C representation and receive their own descriptors, preserving field datatype IDs across model boundaries.

For static XML generation, inline definitions also determine the layouts of XML dependencies. Embedded legacy BSD dictionaries provide fallbacks for types without inline definitions. An explicitly supplied BSD remains authoritative for its imported layouts. A companion type that conflicts with an SDK type name receives a deterministic namespace suffix in its C name. Uppercase companion type names are qualified the same way to avoid SDK macro and factory names. Datatype metadata retains the original XML BrowseName and field spellings.

CMake imports standard types from the schema used to build the library. Direct compiler calls can select that schema directory with --types-schema.

Use copy_types = true to reuse generated code in several servers. The loader reconstructs server-owned descriptors through the same DataTypeDescription loading used by runtime type loading. UA_DataType_toDescription describes each exported static datatype. Nodes, types, member references and encoding IDs share one compile-time namespace plan. UA_Server_addDataTypeFromDescriptionEx applies the same namespace mapping as node loading, resolves members, constructs the C layout, and allocates the server-owned descriptor. Both encoding IDs are supplied as borrowed overrides in the same source context; the server copies and maps them before registering the complete datatype. All linked generated dependencies must use compatible source namespace indexes. Conflicts between imported datatype arrays and a loader’s plan are rejected at C compilation. Use batch generation or supply the same complete NAMESPACE_MAP / --namespaceMap to each generation call. Dependencies must be loaded first, each with its own loader call; neither namespace zero nor all companion specifications need to be loaded in one call. One generated array can contain types from several namespaces. Direct self-references through arrays or optional members are supported by the SDK. Other datatype dependencies are ordered at generation time; mutually recursive definitions and cycles of inline fields are rejected.

Generate several companion modules together with a manifest:

{
  "modules": [
    {"name": "devices", "xml": ["Devices.NodeSet2.xml"]},
    {"name": "machines", "xml": ["Machines.NodeSet2.xml"],
     "depends": ["devices"]}
  ]
}

XML paths are relative to the manifest. Module names use lowercase letters, digits and underscores, starting with a letter. Each module can contain several XML files. Run:

python tools/nodeset_compiler/nodeset_compiler.py \
    --existing path/to/Opc.Ua.NodeSet2.xml --types-array UA_TYPES \
    --batch models.json output-directory

The driver reads all namespace URI tables before generating code, freezes one plan, and emits separate namespace_<name>_generated.c/.h and types_<name>_generated.c/.h files, plus private datatype headers. The chosen plan is saved as namespace_plan.json. Optional --namespaceMap entries reserve indexes; remaining URIs receive unused indexes in dependency/input order. All modules must be regenerated when that plan changes. Generation together does not require loading together: each loader registers only namespaces used by its own inputs and dependencies, and copy mode still supports arbitrary server indexes.

In copy mode, the exported UA_TYPES_<NAME> addresses remain templates. Resolve an exported static descriptor to the server’s descriptor before setting a Variant, encoding a value, or comparing datatype pointers:

size_t ns;
UA_Server_getNamespaceByName(server, UA_STRING("urn:example"), &ns);
UA_NodeId id = UA_TYPES_EXAMPLE[UA_TYPES_EXAMPLE_MYTYPE].typeId;
id.namespaceIndex = (UA_UInt16)ns;
const UA_DataType *type = UA_Server_findDataType(server, &id);

The C structure layouts and generated allocation/copy/clear helpers remain usable. Descriptor addresses returned for different servers need not be equal. The server owns copied descriptors and releases them after its values and nodes. The generated modules must remain loaded until server deletion. Array-specific loading functions and description-conversion helpers are local to the generated namespace .c file and refer directly to the generated arrays. Private *_generated_internal.h headers contain only array declarations and constants. Conversion order is computed at generation time. Application code only includes the public generated headers. Registration uses the generic server datatype APIs. Private symbols include the generated module or datatype-array name so multiple sources can be compiled together, including in an SDK amalgamation. Imported public C types retain their existing names and are reused from dependency headers.

Use copy_types = false to register the exported descriptor addresses directly. The caller must ensure that the baked indexes of datatype IDs, encoding IDs and referenced datatypes already match the server. This path neither validates nor remaps namespace indexes. Use NAMESPACE_MAP (CMake) or --namespaceMap (command line) to choose the baked indexes when needed. For example:

ua_generate_nodeset_and_datatypes(
    NAME "example"
    FILE_NS "Example.NodeSet2.xml"
    NAMESPACE_MAP "2:urn:example" "3:urn:example:encodings")

All generated arrays are const. Without an explicit map, their baked namespace indexes are compiler-local; no-copy loading is valid when the caller arranges matching server indexes. Arrays can be shared by several servers with matching namespace indexes. No arrays are modified and no process-lifetime binding state is needed. Additional namespaces can be appended later.

Generated modules and their dependencies must use matching model definitions and the same SDK configuration. The loader does not compare native layouts at runtime. Static structure members must point at their dependencies’ registered static descriptors. A static consumer of a copied dependency is therefore rejected; use copy mode consistently when servers need different namespace orders. The generated loader links one heap-allocated UA_DataTypeArray entry into config->customDataTypes, with both cleanup and borrowed set to true. Configuration cleanup frees the entry without clearing or freeing the static descriptors. Regenerate the loaders and datatype files for the entire dependency chain when upgrading to this API.

The loader creates ReferenceTypes and raw DataType nodes before registering the types, then creates typed values and finishes the remaining nodes. XML values use the namespace table of their originating XML file, including nested ExtensionObjects, embedded binary bodies, NodeIds and QualifiedNames. Loading several XML files together therefore does not change the meaning of their local namespace indexes.

Copy mode delegates mapping, layout construction and registration to UA_Server_addDataTypeFromDescriptionEx. The SDK owns descriptor storage and cleanup. A failed namespace load removes the nodes explicitly declared by that loader. Application callback side effects follow the existing node lifecycle contract. Namespaces and successfully registered types remain owned by the server and are cleaned up with it. Load each generated namespace once per server. Existing datatype IDs are rejected with BadNodeIdExists in both modes, even when their descriptors match. Loading is not atomic: after a failure that registered types, recreate the server before loading the namespace again. Serialize model-loading calls on each server and do not modify the server’s custom-type chain while its nodes use those descriptors.

A separate BSD type dictionary is still supported. It is only required when the NodeSet2 file does not contain complete Definition elements for its custom datatypes.

We take the following information model snippet as the starting point of the following tutorial. A more detailed tutorial on how to create your own information model and NodeSet2.xml can be found in this blog post: https://profanter.medium.com/how-to-create-custom-opc-ua-information-models-1e9a461f5b58

<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd"
           xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"
           xmlns:s1="http://yourorganisation.org/example_nodeset/"
           xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <NamespaceUris>
        <Uri>http://yourorganisation.org/example_nodeset/</Uri>
    </NamespaceUris>
    <Aliases>
        <Alias Alias="Boolean">i=1</Alias>
        <Alias Alias="UInt32">i=7</Alias>
        <Alias Alias="String">i=12</Alias>
        <Alias Alias="HasModellingRule">i=37</Alias>
        <Alias Alias="HasTypeDefinition">i=40</Alias>
        <Alias Alias="HasSubtype">i=45</Alias>
        <Alias Alias="HasProperty">i=46</Alias>
        <Alias Alias="HasComponent">i=47</Alias>
        <Alias Alias="Argument">i=296</Alias>
    </Aliases>
    <Extensions>
        <Extension>
            <ModelInfo Tool="UaModeler" Hash="Zs8w1AQI71W8P/GOk3k/xQ=="
                       Version="1.3.4"/>
        </Extension>
    </Extensions>
    <UAReferenceType NodeId="ns=1;i=4001" BrowseName="1:providesInputTo">
        <DisplayName>providesInputTo</DisplayName>
        <References>
            <Reference ReferenceType="HasSubtype" IsForward="false">
                i=33
            </Reference>
        </References>
        <InverseName Locale="en-US">inputProcidedBy</InverseName>
    </UAReferenceType>
    <UAObjectType IsAbstract="true" NodeId="ns=1;i=1001"
                  BrowseName="1:FieldDevice">
        <DisplayName>FieldDevice</DisplayName>
        <References>
            <Reference ReferenceType="HasSubtype" IsForward="false">
                i=58
            </Reference>
            <Reference ReferenceType="HasComponent">ns=1;i=6001</Reference>
            <Reference ReferenceType="HasComponent">ns=1;i=6002</Reference>
        </References>
    </UAObjectType>
    <UAVariable DataType="String" ParentNodeId="ns=1;i=1001"
                NodeId="ns=1;i=6001" BrowseName="1:ManufacturerName"
                UserAccessLevel="3" AccessLevel="3">
        <DisplayName>ManufacturerName</DisplayName>
        <References>
            <Reference ReferenceType="HasTypeDefinition">i=63</Reference>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasComponent" IsForward="false">
                ns=1;i=1001
            </Reference>
        </References>
    </UAVariable>
    <UAVariable DataType="String" ParentNodeId="ns=1;i=1001"
                NodeId="ns=1;i=6002" BrowseName="1:ModelName"
                UserAccessLevel="3" AccessLevel="3">
        <DisplayName>ModelName</DisplayName>
        <References>
            <Reference ReferenceType="HasTypeDefinition">i=63</Reference>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasComponent" IsForward="false">
                ns=1;i=1001
            </Reference>
        </References>
    </UAVariable>
    <UAObjectType NodeId="ns=1;i=1002" BrowseName="1:Pump">
        <DisplayName>Pump</DisplayName>
        <References>
            <Reference ReferenceType="HasComponent">ns=1;i=6003</Reference>
            <Reference ReferenceType="HasComponent">ns=1;i=6004</Reference>
            <Reference ReferenceType="HasSubtype" IsForward="false">
                ns=1;i=1001
            </Reference>
            <Reference ReferenceType="HasComponent">ns=1;i=7001</Reference>
            <Reference ReferenceType="HasComponent">ns=1;i=7002</Reference>
        </References>
    </UAObjectType>
    <UAVariable DataType="Boolean" ParentNodeId="ns=1;i=1002"
                NodeId="ns=1;i=6003" BrowseName="1:isOn" UserAccessLevel="3"
                AccessLevel="3">
        <DisplayName>isOn</DisplayName>
        <References>
            <Reference ReferenceType="HasTypeDefinition">i=63</Reference>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasComponent" IsForward="false">
                ns=1;i=1002
            </Reference>
        </References>
    </UAVariable>
    <UAVariable DataType="UInt32" ParentNodeId="ns=1;i=1002"
                NodeId="ns=1;i=6004" BrowseName="1:MotorRPM"
                UserAccessLevel="3" AccessLevel="3">
        <DisplayName>MotorRPM</DisplayName>
        <References>
            <Reference ReferenceType="HasTypeDefinition">i=63</Reference>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasComponent" IsForward="false">
                ns=1;i=1002
            </Reference>
        </References>
    </UAVariable>
    <UAMethod ParentNodeId="ns=1;i=1002" NodeId="ns=1;i=7001"
              BrowseName="1:startPump">
        <DisplayName>startPump</DisplayName>
        <References>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasProperty">ns=1;i=6005</Reference>
            <Reference ReferenceType="HasComponent" IsForward="false">
                ns=1;i=1002
            </Reference>
        </References>
    </UAMethod>
    <UAVariable DataType="Argument" ParentNodeId="ns=1;i=7001" ValueRank="1"
                NodeId="ns=1;i=6005" ArrayDimensions="1"
                BrowseName="OutputArguments">
        <DisplayName>OutputArguments</DisplayName>
        <References>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasProperty"
                       IsForward="false">ns=1;i=7001</Reference>
            <Reference ReferenceType="HasTypeDefinition">i=68</Reference>
        </References>
        <Value>
            <ListOfExtensionObject>
                <ExtensionObject>
                    <TypeId>
                        <Identifier>i=297</Identifier>
                    </TypeId>
                    <Body>
                        <Argument>
                            <Name>started</Name>
                            <DataType>
                                <Identifier>i=1</Identifier>
                            </DataType>
                            <ValueRank>-1</ValueRank>
                            <ArrayDimensions></ArrayDimensions>
                            <Description/>
                        </Argument>
                    </Body>
                </ExtensionObject>
            </ListOfExtensionObject>
        </Value>
    </UAVariable>
    <UAMethod ParentNodeId="ns=1;i=1002" NodeId="ns=1;i=7002"
              BrowseName="1:stopPump">
        <DisplayName>stopPump</DisplayName>
        <References>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasProperty">ns=1;i=6006</Reference>
            <Reference ReferenceType="HasComponent"
                       IsForward="false">ns=1;i=1002</Reference>
        </References>
    </UAMethod>
    <UAVariable DataType="Argument" ParentNodeId="ns=1;i=7002" ValueRank="1"
                NodeId="ns=1;i=6006" ArrayDimensions="1"
                BrowseName="OutputArguments">
        <DisplayName>OutputArguments</DisplayName>
        <References>
            <Reference ReferenceType="HasModellingRule">i=78</Reference>
            <Reference ReferenceType="HasProperty" IsForward="false">
                ns=1;i=7002
            </Reference>
            <Reference ReferenceType="HasTypeDefinition">i=68</Reference>
        </References>
        <Value>
            <ListOfExtensionObject>
                <ExtensionObject>
                    <TypeId>
                        <Identifier>i=297</Identifier>
                    </TypeId>
                    <Body>
                        <Argument>
                            <Name>stopped</Name>
                            <DataType>
                                <Identifier>i=1</Identifier>
                            </DataType>
                            <ValueRank>-1</ValueRank>
                            <ArrayDimensions></ArrayDimensions>
                            <Description/>
                        </Argument>
                    </Body>
                </ExtensionObject>
            </ListOfExtensionObject>
        </Value>
    </UAVariable>
</UANodeSet>

Take the previous snippet and save it to a file myNS.xml. The file is compiled using the following function call in the CMake build system:

ua_generate_nodeset(
    NAME "myNs"
    FILE "${PROJECT_SOURCE_DIR}/examples/nodeset/myNS.xml"
    DEPENDS_TYPES "UA_TYPES"
    DEPENDS_NS "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml
)

If you look into the files generated by the nodeset compiler, you will see that it generated a method called extern UA_StatusCode myNS(UA_Server *server);. You need to include the header and source file and then call myNS(server) right after creating the server instance with UA_Server_new. This will automatically add all the nodes to the server and return UA_STATUSCODE_GOOD if there weren’t any errors. Additionally you need to compile the open62541 stack with the full NS0 by setting UA_NAMESPACE_ZERO=FULL in CMake. Otherwise the stack uses a subset where many nodes are not included and thus adding a custom nodeset may fail.

If you also want to generate custom DataTypes for the nodeset, use the CMake function ua_generate_nodeset_and_datatypes. It uses some best practice settings and you only need to pass a name and the NodeSet2 file. Inline Definition elements generate datatypes automatically. FILE_CSV optionally generates symbolic NodeId constants. FILE_BSD selects the legacy dictionary path when the XML definitions are incomplete. You can also define dependencies between nodesets using the DEPENDS argument. Here are some examples for the DI and PLCOpen nodesets:

# Generate types and namespace for DI
ua_generate_nodeset_and_datatypes(
    NAME "di"
    FILE_CSV "${UA_NODESET_DIR}/DI/Opc.Ua.Di.NodeIds.csv"
    FILE_NS "${UA_NODESET_DIR}/DI/Opc.Ua.Di.NodeSet2.xml"
)

# generate PLCopen namespace which is using DI
ua_generate_nodeset_and_datatypes(
    NAME "plc"
    # PLCopen does not define custom types. Only generate the nodeset
    FILE_NS "${UA_NODESET_DIR}/PLCopen/Opc.Ua.PLCopen.NodeSet2_V1.02.xml"
    # PLCopen depends on the di nodeset, which must be generated before
    DEPENDS "di"
)

Manually calling the Nodeset Compiler

Besides the CMake macros, the underlying Python code of the Nodeset compiler can be called directly. The call looks like this:

$ python ./nodeset_compiler.py --types-array=UA_TYPES \
    --types-output=types_myNS \
    --existing ../../deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml \
    --xml myNS.xml myNS

The output of the command is:

INFO:__main__:Preprocessing (existing) ../../deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml
INFO:__main__:Preprocessing myNS.xml
INFO:__main__:Generating Code
INFO:__main__:NodeSet generation code successfully printed

The first argument --types-array=UA_TYPES defines the name of the global array in open62541 which contains the corresponding types used within the nodeset in NodeSet2.xml. If you do not define your own datatypes, you can always use the UA_TYPES value. More on that later in this tutorial. The next argument --existing ../../deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml points to the XML definition of the standard-defined namespace 0 (NS0). Namespace 0 is assumed to be loaded beforehand and provides definitions for data type, reference types, and so. Since we reference nodes from NS0 in our myNS.xml we need to tell the nodeset compiler that it should also load that nodeset, but not compile it into the output. Note that you may need to initialize the git submodule to get the deps/ua-nodeset folder (git submodule update --init) or download the full NodeSet2.xml manually. The argument --xml myNS.xml points to the user-defined information model, whose nodes will be added to the abstract syntax tree. The script will then create the files myNS.c and myNS.h (indicated by the last argument myNS) containing the C code necessary to instantiate those namespaces. --types-output also creates types_myNS_generated.c and types_myNS_generated.h for inline datatype definitions. Omit that option when only namespace nodes are required or when the datatype array is generated separately from a BSD dictionary.

The help command show which additional options are available:

$ python ./nodeset_compiler.py -h

Creating object instances

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.

Let’s look at an example that will create a pump instance given the newly defined objectType from myNS.xml:

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

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

/* Files myNS.h and myNS.c are created from myNS.xml */
#include "myNS.h"

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(int argc, char **argv) {
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

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

    UA_StatusCode retval = myNS(server, true);
    /* Create nodes from nodeset */
    if(retval != UA_STATUSCODE_GOOD) {
        UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Could not add the example nodeset. "
            "Check previous output for any error.");
        retval = UA_STATUSCODE_BADUNEXPECTEDERROR;
    } else {
        UA_NodeId createdNodeId;
        UA_ObjectAttributes object_attr = UA_ObjectAttributes_default;

        object_attr.description = UA_LOCALIZEDTEXT("en-US", "A pump!");
        object_attr.displayName = UA_LOCALIZEDTEXT("en-US", "Pump1");

        // we assume that the myNS nodeset was added in namespace 2.
        // You should always use UA_Server_addNamespace to check what the
        // namespace index is for a given namespace URI. UA_Server_addNamespace
        // will just return the index if it is already added.
        UA_Server_addObjectNode(server, UA_NODEID_NUMERIC(1, 0),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                                UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                                UA_QUALIFIEDNAME(1, "Pump1"),
                                UA_NODEID_NUMERIC(2, 1002),
                                object_attr, NULL, &createdNodeId);


        retval = UA_Server_run(server, &running);
    }

    UA_Server_delete(server);
    return (int) retval;
}

If you start the server and inspect the nodes, you will find the pump in the objects folder, which look like this Fig. 3.

Instantiated Pump Object with inherited children

Fig. 3 Instantiated Pump Object with inherited children

As you can see the pump has inherited its parents attributes (ManufacturerName and ModelName). Methods, in contrast to objects and variables, are never cloned but instead only linked. The reason is that you will quite probably attach a method callback to a central method, not each object. Objects are instantiated if they are below the object you are creating, so any object (like an object called associatedServer of ServerType) that is part of pump will be instantiated as well. Objects above you object are never instantiated, so the same ServerType object in Fielddevices would have been omitted (the reason is that the recursive instantiation function protects itself from infinite recursions, which are hard to track when first ascending, then redescending into a tree).

Combination of multiple nodesets

In the previous section you have seen how you can use the nodeset compiler with one single nodeset which depends on the default nodeset (NS0) Opc.Ua.NodeSet2.xml. The nodeset compiler also supports nodesets which depend on more than one nodeset. We will show this use-case with the PLCopen nodeset. The PLCopen nodeset Opc.Ua.PLCopen.NodeSet2_V1.02.xml depends on the DI nodeset Opc.Ua.Di.NodeSet2.xml which then depends on NS0. This example is also shown in examples/nodeset/CMakeLists.txt.

For legacy NodeSets whose XML does not contain complete definitions, a BSD dictionary can still be compiled separately. The generated code describes the binary representation required for encoding and decoding. Use the ua_generate_datatypes CMake function, which calls tools/generate_datatypes.py:

ua_generate_datatypes(
    NAME "ua_types_di"
    TARGET_SUFFIX "types-di"
    FILE_CSV "${UA_NODESET_DIR}/DI/Opc.Ua.Di.NodeIds.csv"
    FILES_BSD "${UA_NODESET_DIR}/DI/Opc.Ua.Di.Types.bsd"
)

TARGET_SUFFIX is used to create a new target with the name open62541-generator-TARGET_SUFFIX.

Now you can compile the DI nodeset XML using the following command:

ua_generate_nodeset(
    NAME "di"
    FILE "${UA_NODESET_DIR}/DI/Opc.Ua.Di.NodeSet2.xml"
    TYPES_ARRAY "UA_TYPES_DI"
    INTERNAL
    DEPENDS_TYPES "UA_TYPES"
    DEPENDS_NS    "${UA_NODESET_DIR}/Schema/Opc.Ua.NodeSet2.xml"
    DEPENDS_TARGET "open62541-generator-types-di"
)

There are now two new arguments: INTERNAL indicates that internal headers (and non public API) should be included within the generated source code. This is currently required for nodesets which use structures as data values, and will probably be fixed in the future. The DEPENDS_TYPES types array argument is matched with the nodesets in the same order as they appear on the DEPENDS_TARGET parameter. It tells the nodeset compiler which types array it should use: UA_TYPES for Opc.Ua.NodeSet2.xml and UA_TYPES_DI for Opc.Ua.Di.NodeSet2.xml. This is the type array generated by the generate_datatypes.py script. The rest is similar to the example in previous section: Opc.Ua.NodeSet2.xml is assumed to exist already and only needs to be loaded for consistency checks, Opc.Ua.Di.NodeSet2.xml will be generated in the output file ua_namespace_di.c/.h

Next we can generate the PLCopen nodeset. Since it doesn’t require any additional datatype definitions, we can immediately start with the nodeset compiler command:

ua_generate_nodeset(
    NAME "plc"
    FILE "${UA_NODESET_DIR}/PLCopen/Opc.Ua.PLCopen.NodeSet2_V1.02.xml"
    INTERNAL
    DEPENDS_TYPES
        "UA_TYPES" "UA_TYPES_DI"
    DEPENDS_NS
        "${UA_NODESET_DIR}/Schema/Opc.Ua.NodeSet2.xml"
        "${UA_NODESET_DIR}/DI/Opc.Ua.Di.NodeSet2.xml"
    DEPENDS_TARGET "open62541-generator-ns-di"
)

This call is quite similar to the compilation of the DI nodeset. As you can see, we do not define any specific types array for the PLCopen nodeset. Since the PLCopen nodeset depends on the NS0 and DI nodeset, we need to tell the nodeset compiler that these two nodesets should be seen as already existing. Make sure that the order is the same as in your XML file, e.g., in this case the order indicated in Opc.Ua.PLCopen.NodeSet2_V1.02.xml -> UANodeSet -> Models -> Model.

As a result of the previous scripts you will have multiple source files:

  • ua_types_di_generated.c

  • ua_types_di_generated.h

  • ua_types_di_generated_encoding_binary.h

  • ua_types_di_generated_handling.h

  • ua_namespace_di.c

  • ua_namespace_di.h

  • ua_namespace_plc.c

  • ua_namespace_plc.h

Finally you need to include all these files in your build process and call the corresponding initialization methods for the nodesets. An example application could look like this:

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

/* Create nodes from nodeset */
UA_StatusCode retval = ua_namespace_di(server);
if(retval != UA_STATUSCODE_GOOD) {
    UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
                 "Adding the DI namespace failed. Please check previous error output.");
    UA_Server_delete(server);
    return (int)UA_STATUSCODE_BADUNEXPECTEDERROR;
}

retval |= ua_namespace_plc(server);
if(retval != UA_STATUSCODE_GOOD) {
    UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
                 "Adding the PLCopen namespace failed. Please check previous error output.");
    UA_Server_delete(server);
    return (int)UA_STATUSCODE_BADUNEXPECTEDERROR;
}

retval = UA_Server_run(server, &running);

Outstanding Companion Spec Issues

There are some Companion Specifications that currently cannot be compiled with the Nodeset compiler. Which Companion Specifications are affected and what causes this is described below.

Safety, Glass, DEXPI

Do not specify a BSD file or BSD blob in the XML file. The BSD file is considered deprecated. However, it is currently still required by the Nodeser compiler.

I4AAS, RSL, FDI

Attempting to load will result in a runtime error (“Type-checking failed with error code BadTypeMismatch” or “Parent node not found”).

BACnet

Defines data types whose fields have the names signed or unsigned. This leads to errors when creating C structures, because signed and unsigned are keywords in C.

Automatic Nodesetinjection

The nodesetinjector is a mechanism for automatically loading nodeset/companion specifications during server initialization. It provides a fast and easy way to load nodesets in all applications, focusing on the official OPCFoundation/UANodeset Repository (https://github.com/OPCFoundation/UA-Nodeset). Specify the required information models using CMake.

Which nodesets are to be loaded is determined by the Cmake flag DUA_INFORMATION_MODEL_AUTOLOAD. All nodesets that are to be loaded automatically are listed here. The naming is based on the folder name of the Companion Specification in the ua-nodeset folder.

A CMake call could look like this.

-DCMAKE_BUILD_TYPE=Debug
-DUA_BUILD_EXAMPLES=ON
-DUA_INFORMATION_MODEL_AUTOLOAD=DI;POWERLINK;PROFINET;MachineVision
-DUA_NAMESPACE_ZERO=FULL

The order of nodesets is important! Nodesets that build on other nodesets must be placed after them in the list. The following nodesets are currently supported, grouped by dependency depth:

Level 0 — Standalone (no companion-spec dependency, only ns0):

AMB, BACnet, CNC, DEXPI, DI, GDS, I4AAS, IOLinkIODD, IREDES, ISA95-JOBCONTROL, MachineVision, Machinery-Result, MDIS, MTConnect, OpenSCS, PackML, PROFINET, Safety, Scheduler, UAFX-Data, WireHarness-VEC, WoT

Level 1 — Depends on Level-0 specs (typically DI):

ADI (DI), AutoID (DI), CommercialKitchenEquipment (DI), CSPPlusForMachine (DI), FDI5 (DI), FDI7 (DI), FDT (DI), IA (DI), IOLink (DI), Machinery-Jobs (ISA95-JOBCONTROL), Onboarding (GDS), PADIM (DI), PlasticsRubber-GeneralTypes (DI), PLCopen (DI), PNEM (DI), PNENC (DI), PNGSDGM (DI), PNRIO (DI), POWERLINK (DI), Robotics (DI), Sercos (DI), UAFX-AC (DI, UAFX-Data), UAFX-CM (DI, UAFX-Data)

Level 2 — Depends on Level-1 specs (two or more dependency hops):

Each entry lists its required dependencies in parentheses.

CAS (DI, Machinery, IA), ECM (DI, IA), Eumabois (DI, Machinery, Woodworking), Glass (DI, Machinery), LADS (DI, AMB, Machinery), Machinery (DI, IA), Machinery-Energy (DI, IA, ECM), Machinery-ProcessValues (DI, PADIM), MachineTool (DI, Machinery, IA, ISA95-JOBCONTROL, Machinery-Jobs), Mining-General (DI, IA, Machinery), PNDRV (DI, PNENC), PlasticsRubber-TCD (DI, PlasticsRubber-GeneralTypes), PlasticsRubber-IMM2MES (DI, PlasticsRubber-GeneralTypes), PlasticsRubber-LDS (DI, PlasticsRubber-GeneralTypes), PlasticsRubber-HotRunner (DI, PlasticsRubber-GeneralTypes), Pumps (DI, Machinery), Scales (DI, Machinery, PackML), Shotblasting (DI, Machinery, ISA95-JOBCONTROL, Machinery-Jobs), STGeneralTypes (DI, IA, Machinery, ISA95-JOBCONTROL, Machinery-Jobs), SurfaceTechnology (DI, IA, Machinery, ISA95-JOBCONTROL, Machinery-Jobs), TMC (DI, PackML), Weihenstephan (DI, Machinery, PackML), Woodworking (DI, Machinery, ISA95-JOBCONTROL, Machinery-Jobs)

Level 3 — Deep stacks (depends on Level-2 specs):

Each entry lists its full dependency chain in parentheses, i.e. every spec that must be loaded before it.

AdditiveManufacturing (DI, IA, Machinery, PADIM, Machinery-ProcessValues, ISA95-JOBCONTROL, Machinery-Jobs, MachineTool), CuttingTool (DI, IA, Machinery, ISA95-JOBCONTROL, Machinery-Jobs, Machinery-Result, MachineTool, GMS), GMS (DI, IA, Machinery, ISA95-JOBCONTROL, Machinery-Jobs, Machinery-Result, MachineTool), IJT (DI, AMB, IA, Machinery, Machinery-Result — loads both IJT/Base and IJT/Tightening), LaserSystems (DI, IA, Machinery, ISA95-JOBCONTROL, Machinery-Jobs, MachineTool), MetalForming (DI, IA, Machinery, PADIM, Machinery-ProcessValues, ISA95-JOBCONTROL, Machinery-Jobs, MachineTool), Mining-Extraction-General (DI, IA, Machinery, Mining-General), Mining-Extraction-ShearerLoader (DI, IA, Machinery, Mining-General, Mining-Extraction-General), Mining-Loading-General (DI, IA, Machinery, Mining-General), Mining-Loading-HydraulicExcavator (DI, IA, Machinery, Mining-General, Mining-Loading-General), Mining-DevelopmentSupport-General (DI, IA, Machinery, Mining-General), Mining-DevelopmentSupport-RoofSupportSystem (DI, IA, Machinery, Mining-General, Mining-DevelopmentSupport-General), Mining-DevelopmentSupport-Dozer (DI, IA, Machinery, Mining-General, Mining-DevelopmentSupport-General), Mining-TransportDumping-General (DI, IA, Machinery, Mining-General), Mining-TransportDumping-RearDumpTruck (DI, IA, Machinery, Mining-General, Mining-TransportDumping-General), Mining-TransportDumping-ArmouredFaceConveyor (DI, IA, Machinery, Mining-General, Mining-TransportDumping-General), Mining-MineralProcessing-General (DI, IA, Machinery, Mining-General), Mining-MineralProcessing-RockCrusher (DI, IA, Machinery, Mining-General, Mining-MineralProcessing-General), Mining-PELOServices-General (DI, IA, Machinery, Mining-General), Mining-PELOServices-FaceAlignmentSystem (DI, IA, Machinery, Mining-General, Mining-PELOServices-General), Mining-MonitoringSupervisionServices-General (DI, IA, Machinery, Mining-General), PAEFS (DI, Machinery, PADIM, Machinery-ProcessValues), Powertrain (DI, Machinery, UAFX-Data, UAFX-AC), WMTP (DI, Machinery, PADIM, Machinery-ProcessValues), PlasticsRubber-Extrusion-GeneralTypes (DI, IA, Machinery, PlasticsRubber-GeneralTypes), PlasticsRubber-Extrusion-{ExtrusionLine,Extruder,Die,Filter,MeltPump,HaulOff, Pelletizer,Calender,Calibrator,Corrugator,Cutter} (DI, IA, Machinery, PlasticsRubber-GeneralTypes, PlasticsRubber-Extrusion-GeneralTypes), PlasticsRubber-Extrusion_v2-GeneralTypes (DI, IA, Machinery, PlasticsRubber-GeneralTypes), PlasticsRubber-Extrusion_v2-{ExtrusionLine,Extruder,Die,Filter,MeltPump,HaulOff, Pelletizer,Calender,Calibrator,Corrugator,Cutter} (DI, IA, Machinery, PlasticsRubber-GeneralTypes, PlasticsRubber-Extrusion_v2-GeneralTypes), TTD (DI, IA, Machinery, ISA95-JOBCONTROL, Machinery-Jobs, Machinery-Result, MachineTool), WireHarness (DI, Machinery, ISA95-JOBCONTROL, Machinery-Jobs, Machinery-Result, WireHarness-VEC)

Note

PADIM and Machinery-ProcessValues are generated as internal helper nodesets (including the IRDI dictionary) when a model that requires them is selected.

Warning

Some companion specifications are not yet supported:

  • GPOS and RSL depend on OPC UA Part 8 spatial types (3DFrame, 3DCartesianCoordinates) and use abstract VariableTypes as TypeDefinitions, which are not yet fully supported by the open62541 type system.

  • Glass has no embedded .bsd blob, so the compile-time nodeset compiler cannot generate UA_DataType definitions for its custom structs. Loading Glass via the NodesetLoader at runtime works (the loader registers types from <Definition> elements dynamically).

Some companion specifications define types with the same C name in different OPC UA namespaces. These cannot be loaded simultaneously:

  • Pumps and PAEFS both define UA_ControlModeEnum.

  • TMC and PlasticsRubber-GeneralTypes both define UA_ControlModeEnumeration / UA_ProductionStatusEnumeration.

  • CommercialKitchenEquipment and PlasticsRubber-TCD both define UA_OperatingModeEnumeration.

  • PlasticsRubber-LDS and PlasticsRubber-Extrusion-GeneralTypes both define UA_ComponentStatusEnumeration.

  • PlasticsRubber-Extrusion (v1) and PlasticsRubber-Extrusion_v2 share multiple type names and cannot be loaded together.

  • MachineVision and Machinery-Result both define ProcessingTimesDataType / ResultDataType. Choose Machinery-Result when IJT, GMS, or TTD are needed.

  • PNEM and ECM both define AcPeDataType / AcPpDataType / EnergyStateInformationDataType. Choose ECM when Machinery-Energy is needed.

When the open62541 library is installed on the system, the automatically generated autoinject library is installed alongside it. Additionally, the header files for the specified nodesets will be accessible on the system.

Integrating the autoinject library into an existing CMake project is straightforward. To achieve this, the CMake configuration should include the following:

set(open62541_LIBRARIES "")
find_package(open62541 REQUIRED)
list(APPEND open62541_LIBRARIES open62541::open62541 open62541::autoinject)

The generated namespace header files can also be included and used with the following code:

#include <autoinject/namespace_di_generated.h>
#include <autoinject/namespace_amb_generated.h>
#include <autoinject/namespace_machinery_generated.h>