brief introduction
When writing an application, it is more comfortable to use some GUI tools to create an information model. Most tools can export data according to OPC UA Nodeset XML schema. open62541 includes a python based nodeset compiler, which can transform these information model definitions into a working server.
Note that you can use tools / nodeset_ The nodeset compiler found in the compiler subfolder is not an XML conversion tool, but a compiler. This means that it will create an internal representation when parsing the XML file and try to understand and verify the correctness of the representation in order to generate C code.
target
Translation and supplement open62541v1 2 official manual.
start-up
Let's use the following information model fragment as a starting point for the following tutorial. About how to create your own information model and nodeset2 A more detailed tutorial on XML can be found in this blog post: https://opcua.rocks/custom-information-models/
<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 out the previous clip and save it to a file myns XML. In order to compile this node set into the corresponding C code, which can then be used by the open62541 stack, the node set compiler needs some parameters when you call it. The output of the help command gives you the following information.
$ python ./nodeset_compiler.py -h usage: nodeset_compiler.py [-h] [-e <existingNodeSetXML>] [-x <nodeSetXML>] [--internal-headers] [-b <blacklistFile>] [-i <ignoreFile>] [-t <typesArray>] [-v] <outputFile> positional arguments: <outputFile> The path/basename for the <output file>.c and <output file>.h files to be generated. This will also be the function name used in the header and c-file. optional arguments: -h, --help show this help message and exit -e <existingNodeSetXML>, --existing <existingNodeSetXML> NodeSet XML files with nodes that are already present on the server. -x <nodeSetXML>, --xml <nodeSetXML> NodeSet XML files with nodes that shall be generated. --internal-headers Include internal headers instead of amalgamated header -b <blacklistFile>, --blacklist <blacklistFile> Loads a list of NodeIDs stored in blacklistFile (one NodeID per line). Any of the nodeIds encountered in this file will be removed from the nodeset prior to compilation. Any references to these nodes will also be removed -i <ignoreFile>, --ignore <ignoreFile> Loads a list of NodeIDs stored in ignoreFile (one NodeID per line). Any of the nodeIds encountered in this file will be kept in the nodestore but not printed in the generated code -t <typesArray>, --types-array <typesArray> Types array for the given namespace. Can be used mutliple times to define (in the same order as the .xml files, first for --existing, then --xml) the type arrays --max-string-length MAX_STRING_LENGTH Maximum allowed length of a string literal. If longer, it will be set to an empty string -v, --verbose Make the script more verbose. Can be applied up to 4 times
Therefore, the final call looks like this.
$ python ./nodeset_compiler.py --types-array=UA_TYPES --existing ../../deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml --xml myNS.xml myNS
And the output of the command.
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
First parameter -- Types array = UA_TYPES defines the name of the global array in open62541, which contains nodeset2 The corresponding type used within the node set in XML. If you don't define your own data type, you can always use UA_TYPES value. There will be more on this later in this tutorial. Next parameter -- existing.. // deps/ua-nodeset/Schema/Opc. Ua. NodeSet2.xml points to the XML definition of namespace 0 (NS0) of the standard definition. Namespace 0 is considered pre loaded and provides definitions of data types, reference types, etc. Because we are in myNS The node of NS0 is referenced in XML. We need to tell the node set compiler that it should also load the node set, but do not compile it into the output. Note that you may need to initialize the git sub module to get the DEPs / UA nodeset folder (git sub module update -- init) or manually download the complete nodeset2 xml. Parameter -- XML myNS XML points to a user-defined information model whose nodes will be added to the abstract syntax tree. The script then creates myNS C and myNS H file (represented by the last parameter myNS), which contains the C code required to instantiate these namespaces.
It is highly improbable for the compiler to run this way. If you want to check cmakelists Txt (examples/nodeset/CMakeLists.txt), you will find the file server_nodeset.xml is compiled with the following function.
ua_generate_nodeset( NAME "example" FILE "${PROJECT_SOURCE_DIR}/examples/nodeset/server_nodeset.xml" DEPENDS_TYPES "UA_TYPES" DEPENDS_NS "${UA_FILE_NS0}" )
If you look at the file generated by the nodeset compiler, you will find that it generates an external UA_ StatusCode myNS(UA_Server *server); Methods. You need to include the header file and source file, and then call the myNS(server) method immediately after creating the server instance with "UA_Server_new". This will automatically add all nodes to the server and return "ua_status_good" if there are no errors. In addition, you need to set UA in CMake_ NAMESPACE_ Zero = full to compile the open62541 stack of the complete NS0. Otherwise, the stack uses a subset in which many nodes are not included, so adding a custom node set may fail.
This is how you use the node set compiler to compile simple NodeSet XMLs to be used by the open62541 stack.
For your convenience and simpler use, we also provide a CMake function, which simplifies ua_generate_datatypes and UA_ generate_ Use of nodeset function. This function is strongly recommended: ua_generate_nodeset_and_datatypes'. It uses some best practice settings. You only need to pass a name, namespace mapping NAMESPACE_MAP ` (as further described below) and node set files. Transmission csv and bsd file is optional. If not, the data type generated for the note set will be skipped. You can also use the "demands" parameter to define the dependencies between node sets.
Here are some examples of DI and PLCOpen node sets.
# Generate types and namespace for DI ua_generate_nodeset_and_datatypes( NAME "di" FILE_CSV "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/DI/OpcUaDiModel.csv" FILE_BSD "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/DI/Opc.Ua.Di.Types.bsd" NAMESPACE_MAP "2:http://opcfoundation.org/UA/DI/" FILE_NS "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/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 "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/PLCopen/Opc.Ua.Plc.NodeSet2.xml" # PLCopen depends on the di nodeset, which must be generated before DEPENDS "di" )
Create object instance
One of the main benefits of defining object types is that it is fairly easy to create object instances. When the typedefinition NodeId points to a valid objectType node, the object instance will be processed automatically. All properties and methods contained in the objectType definition will be instantiated together with the object node.
Although variables can be copied from the objectType definition (for example, allowing users to attach new data sources to them), methods are always linked only. This paradigm is the same as languages such as C + +. The called method is always the same piece of code, but the first parameter is a pointer to the object. Similarly, in OPC UA, only one methodCallback can be attached to a specific methodNode. If the method node is called, the parent object Id will be passed to the method - at that moment, the method's job is to release which object instance it belongs to.
Let's look at an example that will create an instance of a pump given from myns The newly defined object type of 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); /* 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; }
Make sure you have updated the header file and libs in the project, and then recompile and run the server. In particular, make sure you have myns Add h to your include folder.
As you can see, instantiating an object is no different from creating an object node. The main difference is that you must * use an objectType node as a typeDefinition.
If you start the server and use the UA expert to check the node, you will find the pump in the object folder. It looks like this Figure 2.
As you can see, the pump has inherited its parent properties (ManufacturerName and ModelName). Methods, as opposed to objects and variables, are never cloned, but just linked. The reason is that you are likely to attach method callbacks to a central method instead of each object. If the object is below the object you are creating, these objects will be instantiated, so any object that is part of the pump (such as an object of Server type called associatedServer) will also be instantiated. The object will never be instantiated on your object, so the same ServerType object in Fielddevices will be omitted (because the recursive instantiation function protects itself from infinite recursion. It is difficult to follow when it rises for the first time and then falls back to a tree).
Combination of multiple node sets
In the previous section, you saw how to use the node set compiler, which depends on the default node set (NS0) OPC Ua. NodeSet2. A single node set of XML. The node set compiler also supports node sets that depend on more than one node set. We will use the PLCopen node set to illustrate this use case. PLCopen node set OPC Ua. Plc. NodeSet2. XML depends on the DI node set OPC Ua. DI. NodeSet2. XML, and then rely on NS0. This example is also shown in examples / nodeset / cmakelists txt.
This DI node set uses DEPs / UA nodeset / DI / OPC Ua. DI. Types. Some additional data types in BSD. Since we also need these types in the generated code, we first need to compile these types into C code. The generated code mainly defines the binary representation of the types required for encoding and decoding. Generation can use ua_generate_datatypes CMake function, which uses tools/generate_datatypes.py script.
ua_generate_datatypes( NAME "ua_types_di" TARGET_SUFFIX "types-di" NAMESPACE_MAP "2:http://opcfoundation.org/UA/DI/" FILE_CSV "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/DI/OpcUaDiModel.csv" FILES_BSD "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/DI/Opc.Ua.Di.Types.bsd" )
The parameter "NAMESPACE_MAP" is a string array that represents the mapping of a specific namespace to the resulting namespace index. This mapping is required for the correct mapping of DataType nodes and their node ID s. At present, we need to rely on the namespace to be added to this location of the final server. At present, there is no automatic inference function (pull request is warmly welcome). If you're in UA_ generate_ nodeset_ and_ Using the demands option on datatypes, namespace_ Map will also be inherited. You don't need to pass all mappings for dependent types. CSV and BSD files contain metadata and definitions of types. TARGET_SUFFIX is used to create a new target named open62541 generator TARGET_SUFFIX.
Now you can compile DI node set XML with the following command.
ua_generate_nodeset( NAME "di" FILE "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/DI/Opc.Ua.Di.NodeSet2.xml" TYPES_ARRAY "UA_TYPES_DI" INTERNAL DEPENDS_TYPES "UA_TYPES" DEPENDS_NS "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml" DEPENDS_TARGET "open62541-generator-types-di" )
Now there are two new parameters. International means that INTERNAL header files (and non-public API s) should be included in the generated source code. At present, this is necessary for node sets that use structures as data values, and may be repaired in the future. DEPENDS_TYPES array parameters and node sets are matched in the same order as they appear in demands_ The order on the target parameter is the same. It tells the node set compiler which type of array to use. UA_TYPES for OPC Ua. NodeSet2. xml,UA_TYPES_DI for OPC Ua. Di. NodeSet2. xml. This is by generate_ datatypes. Type array generated by py script. The rest is similar to the example in the previous section: OPC Ua. NodeSet2. XML is considered to exist, and only the consistency check needs to be loaded, OPC Ua. Di. NodeSet2. XML will be in the output file UA_ namespace_ di. c/. Generated in H.
Next, we can generate the PLCopen node set. Since it does not require any additional data type definitions, we can start using the node set compiler command immediately.
ua_generate_nodeset( NAME "plc" FILE "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/PLCopen/Opc.Ua.Plc.NodeSet2.xml" INTERNAL DEPENDS_TYPES "UA_TYPES" "UA_TYPES_DI" DEPENDS_NS "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/Schema/Opc.Ua.NodeSet2.xml" "${PROJECT_SOURCE_DIR}/deps/ua-nodeset/DI/Opc.Ua.Di.NodeSet2.xml" DEPENDS_TARGET "open62541-generator-ns-di" )
This call is similar to the compilation of the DI node set. As you can see, we do not define any specific type array for the PLCopen node set. Since the PLCopen node set depends on the NS0 and DI node sets, we need to tell the node set compiler that these two node sets should be considered to exist. Make sure the order is the same as your XML file, for example, in this case, in OPC Ua. Plc. NodeSet2. The order indicated in XML - > uanodeset - > models - > model.
As a result of the previous script, 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 node set. An example of an application can be 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);