Stefan Profanter 1bd6943acf Set default logging level to error 6 years ago
..
NodeID_AssumeExternal.txt c4c2c6f316 set executable bit on python scripts 7 years ago
NodeID_Blacklist.txt 0e9b6caeb2 Basis for server bootstrapping 7 years ago
NodeID_NS0_Base.txt 2008205183 Fix list of manually bootstrapped nodes 6 years ago
README.md 983f956003 Fix typos 7 years ago
__init__.py 3ea21ea597 Align CMake and use new parameters for types generator 7 years ago
backend_graphviz.py 7a4996743a Fix #1346 force encoding to utf-8 7 years ago
backend_open62541.py f777fe276a Simplify node sorting rules in the nodeset compiler 6 years ago
backend_open62541_datatypes.py 5a3260ed58 Nodeset compiler: Improve support for utf8 strings in generated code 6 years ago
backend_open62541_nodes.py f777fe276a Simplify node sorting rules in the nodeset compiler 6 years ago
constants.py 9d582a7d13 Fix node ordering and successfully generate code for full NS0 7 years ago
datatypes.py 2706aac3a2 Include node ids in error output 6 years ago
nodes.py 1473e47792 Prefer HasSubtype for the parent ref 6 years ago
nodeset.py f777fe276a Simplify node sorting rules in the nodeset compiler 6 years ago
nodeset_compiler.py 1bd6943acf Set default logging level to error 6 years ago
nodeset_testing.py 7a4996743a Fix #1346 force encoding to utf-8 7 years ago
opaque_type_mapping.py 3ea21ea597 Align CMake and use new parameters for types generator 7 years ago

README.md

pyUANamespace

pyUANamespace is a collection of python 2 scripts that can parse OPC UA XML Namespace definition files and transform them into a class representation. This facilitates both reprinting the namespace in a different non-XML format (such as C-Code or DOT) and analysis of the namespace structure.

Documentation

The pyUANamespace implementation has been contributed by a research project of the chair for Process Control Systems Engineering of the TU Dresden. It was not strictly speaking created as a C generator, but could be easily modified to fulfill this role for open62541.

Functionality in open62541

In open62541, the linked python namespace generated by the pyUANamespace classes are used to print C-Code that will automatically initialize a server. Apart from parsing XML, most classes also have a printOpen62541Header() or printOpen62541Header_Subtype() function that can reprint the node as C Code compatible with the project.

This function has been wrapped into the generate_open62541CCode.py program, which implements the compiler and option checking relevant to this task. The program is called as follows:

$ python generate_open62541CCode.py /path/to/NodeSet.xml /path/to/outputfile.c

Several options have been made available to further facilitate header generation. Calling the script without arguments will produce a usage helper listing all available options.

Overwriting NodeSet definitions

Most notably, any number of XML files can be passed. The latest XML definition of a node found is used.

$ python generate_open62541CCode.py /path/to/NodeSet0.xml /path/to/OverwriteNodeSet0.xml /path/to/outputfile.c

Blacklisting Nodes

If a set of nodes is not to be contained in the namespace, they can be specified as a blacklist file containing one nodeId per line in the following format:

ns=1;id=2323
id=11122;

This file can be passed to the compiler, which will remove these nodes prior to linking the namespace.

$ python generate_open62541CCode.py -b blacklist.txt /path/to/NodeSet.xml /path/to/outputfile.c

In this particular example, nodes ns=1;id=2323 and ns=0;id=11122 will be removed from the namespace (which may invalidate other nodes referring to them).

Ignoring Nodes

Blacklisting removes nodes, which means that any other nodes referring to these nodes will contain invalid references. If a namespace should be generated that can use all datatypes, objectstypes, etc., but should only print specific nodes into the C Header, a set of nodes can be ignored. This will cause the compiler to use them in the linked namespace where other nodes can use them (e.g. in buildEncodingRules()), but they will not appear as part of the header file.

Ignorelists have the same format as blacklists and are passed to the compiler like so:.

$ python generate_open62541CCode.py -i ignorelist.txt /path/to/NodeSet.xml /path/to/outputfile.c

Given the blacklist example, the nodes ns=1;id=2323 and ns=0;id=11122 will not be part of the header, but other nodes may attempt to create references to them or use them as dataTypes.

Suppressing attributes

Most of OPC UA Namespaces depend heavily on strings. These can bloat up memory usage in applications where memory is a critical resource. The compiler can be instructed to suppress allocation for certain attributes, which will be initialized to sensible defaults or NULL pointers where applicable.

$ python generate_open62541CCode.py -s browsename -s displayname -s nodeid /path/to/NodeSet.xml /path/to/outputfile.c

Currently, the following base attributes of nodes can be suppressed:

  • browseName
  • displayName
  • description
  • nodeId
  • writeMask
  • userWriteMask

Further attributes may be added at a later point depending on demand.

Core functionality

OPC UA node types, base data types and references are described in ua_node_types.py and ua_builtin_types.py. These classes are primarily intended to act as part of an AST to parse OPC UA Namespace description files. They implement a hierarchic/recursive parsing of XML DOM objects, supplementing their respective properties from the XML description.

A manager class called NodeSet is included in the respective source file. This class does not correspond to a OPC UA Namespace. It is an aggregator and manager for nodes and references which may belong to any number of namespaces. This class includes extensive parsing/validation to ensure that a complete and consistent namespace is generated.

Namespace compilation internals

Compiling a namespace consists of the following steps:

  • Read one or more XML definitions
  • Link references to actual nodes (also includes references to dataTypes, etc.)
  • Sanitize/Remove any nodes and references that could not be properly parsed
  • Derive encoding rules for datatypes
  • Parse/Allocate variable values according to their dataType definitions

Reading and parsing XML files is handled by NodeSet.parseXML(/path/to/file.xml). It is the first part of a multipass compiler that will create all nodes contained in the XML description.

During the reading of XML files, nodes will attempt to parse any attributes they own, but not refer to any other nodes. References will be kept as XML descriptions. Any number of XML files can be read in this phase. NOTE: In the open62541 (this) implementation, duplicate nodes (same NodeId) are allowed. The last definition encountered will be kept. This allows overwriting specific nodes of a much larger XML file to with implementation specific nodes.

The next phase of the compilation is to link all references. The phase is called by NodeSet.linkOpenPointers(). All references will attempt to locate their target() node in the namespace and point to it.

During the sanitation phase called by NodeSet.sanitize(), nodes check themselves for invalid attributes. Most notably any references that could not be resolved to a node will be removed from the nodes.

When calling NodeSet.buildEncodingRules(), dataType nodes are examined to determine if and how the can be encoded as a serialization of OPC UA builtin types as well as their aliases.

The following fragment of a variable value can illustrate the necessity for determining encoding rules:

<Argument>
    <Name>ServerHandles</Name>
    <DataType>
    <Identifier>i=7</Identifier>
    </DataType>
    <ValueRank>1</ValueRank>
    <ArrayDimensions />
    <Description p5:nil="true" xmlns:p5="http://www.w3.org/2001/XMLSchema-instance" />
</Argument>

The tags body, TypeId, Identifier, and Argument are aliases for builtin encodings or structures thereof. Without knowing which type they represent, an appropriate value class (and with that a parser) cannot be created. pyUANamespace will resolve this specific case by determining the encoding as follows:

LastMethodOutputArguments : Argument -> i=296
+ [['Name', ['String']], ['DataType', ['NodeId']], ['ValueRank', ['Int32']], ['ArrayDimensions', ['UInt32']], ['Description', ['LocalizedText']]] (already analyzed)

DataTypes that cannot be encoded as a definite serial object (e.g., by having a member of NumericType, but not a specific one), will have their isEncodable() attribute disabled. All dataTypes that complete this node can be used to effectively determine the size and serialization properties of any variables.

Having encoding rules means that data can now be parsed when a tag is encountered in a description. Calling NodeSet.allocateVariables() will do just that for any variable node that holds XML Values.

The result of this compilation is a completely linked and instantiated OPC UA namespace.

An example compiler can be built as follows:

class testing:
  def __init__(self):
    self.namespace = NodeSet("testing")

    log(self, "Phase 1: Reading XML file nodessets")
    self.namespace.parseXML("Opc.Ua.NodeSet2.xml")
    self.namespace.parseXML("DeviceNodesSet.xml")
    self.namespace.parseXML("MyNodesSet.xml")

    log(self, "Phase 2: Linking address space references and datatypes")
    self.namespace.linkOpenPointers()
    self.namespace.sanitize()

    log(self, "Phase 3: Comprehending DataType encoding rules")
    self.namespace.buildEncodingRules()

    log(self, "Phase 4: Allocating variable value data")
    self.namespace.allocateVariables()