From 0a6ccdbe44d306fd47c960ad84e6f324ba59e731 Mon Sep 17 00:00:00 2001 From: Stefan Profanter Date: Tue, 1 Oct 2019 12:58:17 +0200 Subject: [PATCH] feat(server): Integrate node id header generation into nodeset compiler --- tools/cmake/macros_public.cmake | 24 ++++-- tools/nodeset_compiler/backend_open62541.py | 87 ++++++++++++++++++++- tools/nodeset_compiler/nodes.py | 21 +++++ tools/nodeset_compiler/nodeset_compiler.py | 24 ++++-- 4 files changed, 142 insertions(+), 14 deletions(-) diff --git a/tools/cmake/macros_public.cmake b/tools/cmake/macros_public.cmake index 0710dad06..e0e41d25d 100644 --- a/tools/cmake/macros_public.cmake +++ b/tools/cmake/macros_public.cmake @@ -229,6 +229,7 @@ endfunction() # Options: # # [INTERNAL] Optional argument. If given, then the generated node set code will use internal headers. +# [GENERATE_IDS] Optional argument. If given, then the generated code will include define directives for numeric node ids # # Arguments taking one value: # @@ -252,7 +253,7 @@ endfunction() # function(ua_generate_nodeset) - set(options INTERNAL ) + set(options INTERNAL GENERATE_IDS) set(oneValueArgs NAME TYPES_ARRAY OUTPUT_DIR IGNORE TARGET_PREFIX BLACKLIST) set(multiValueArgs FILE DEPENDS_TYPES DEPENDS_NS DEPENDS_TARGET) cmake_parse_arguments(UA_GEN_NS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) @@ -311,6 +312,11 @@ function(ua_generate_nodeset) set(GEN_IGNORE "--ignore=${UA_GEN_NS_IGNORE}") endif() + set(GEN_IDS "") + if (UA_GEN_NS_GENERATE_IDS) + set(GEN_IDS "--generate-ids") + endif() + set(TYPES_ARRAY_LIST "") foreach(f ${UA_GEN_NS_DEPENDS_TYPES}) # Replace dash with underscore to make valid c literal @@ -338,13 +344,17 @@ function(ua_generate_nodeset) file(MAKE_DIRECTORY ${UA_GEN_NS_OUTPUT_DIR}) endif() + string(REPLACE "-" "_" UA_GEN_NS_NAME ${UA_GEN_NS_NAME}) + string(TOUPPER "${UA_GEN_NS_NAME}" GEN_NAME_UPPER) + add_custom_command(OUTPUT ${UA_GEN_NS_OUTPUT_DIR}/namespace${FILE_SUFFIX}.c ${UA_GEN_NS_OUTPUT_DIR}/namespace${FILE_SUFFIX}.h PRE_BUILD COMMAND ${PYTHON_EXECUTABLE} ${open62541_TOOLS_DIR}/nodeset_compiler/nodeset_compiler.py ${GEN_INTERNAL_HEADERS} ${GEN_NS0} - ${GEN_BIN_SIZE} + ${GEN_IDS} + --ids-prefix=${GEN_NAME_UPPER} ${GEN_IGNORE} ${GEN_BLACKLIST} ${TYPES_ARRAY_LIST} @@ -376,9 +386,6 @@ function(ua_generate_nodeset) set_source_files_properties(${UA_GEN_NS_OUTPUT_DIR}/namespace${FILE_SUFFIX}.c PROPERTIES LANGUAGE CXX) endif() - string(REPLACE "-" "_" UA_GEN_NS_NAME ${UA_GEN_NS_NAME}) - string(TOUPPER "${UA_GEN_NS_NAME}" GEN_NAME_UPPER) - set_property(GLOBAL PROPERTY "UA_GEN_NS_DEPENDS_FILE_${UA_GEN_NS_NAME}" ${UA_GEN_NS_DEPENDS_NS} ${UA_GEN_NS_FILE}) set_property(GLOBAL PROPERTY "UA_GEN_NS_DEPENDS_TYPES_${UA_GEN_NS_NAME}" ${UA_GEN_NS_DEPENDS_TYPES} ${UA_GEN_NS_TYPES_ARRAY}) @@ -419,7 +426,7 @@ endfunction() # NAME Short name of the nodeset. E.g. 'di' # FILE_NS Path to the NodeSet2.xml file. Multiple values can be passed. These nodesets will be combined into one output. # -# [FILE_CSV] Optional path to the .csv file containing the node ids, e.g. 'OpcUaDiModel.csv' +# [FILE_CSV] Optional path to the .csv file containing the node ids, e.g. 'OpcUaDiModel.csv'. If not given, the nodeid defines will automatically be generated # [FILE_BSD] Optional path to the .bsd file containing the type definitions, e.g. 'Opc.Ua.Di.Types.bsd'. Multiple files can be # passed which will all combined to one resulting code. # [IMPORT_BSD] Optional combination of types array and path to the .bsd file containing additional type definitions referenced by @@ -552,6 +559,10 @@ function(ua_generate_nodeset_and_datatypes) if (${UA_GEN_INTERNAL}) set(NODESET_INTERNAL "INTERNAL") endif() + set(NODESET_GENIDS "") + if("${UA_GEN_FILE_CSV}" STREQUAL "") + set(NODESET_GENIDS "GENERATE_IDS") + endif() ua_generate_nodeset( NAME "${UA_GEN_NAME}" @@ -559,6 +570,7 @@ function(ua_generate_nodeset_and_datatypes) TYPES_ARRAY "${NODESET_TYPES_ARRAY}" BLACKLIST "${UA_GEN_BLACKLIST}" ${NODESET_INTERNAL} + ${NODESET_GENIDS} DEPENDS_TYPES ${TYPES_DEPENDS} DEPENDS_NS ${NODESET_DEPENDS} DEPENDS_TARGET ${NODESET_DEPENDS_TARGET} diff --git a/tools/nodeset_compiler/backend_open62541.py b/tools/nodeset_compiler/backend_open62541.py index d9358ae71..01047567d 100644 --- a/tools/nodeset_compiler/backend_open62541.py +++ b/tools/nodeset_compiler/backend_open62541.py @@ -14,6 +14,7 @@ from __future__ import print_function from os.path import basename import logging import codecs +import re import os try: from StringIO import StringIO @@ -122,7 +123,86 @@ def sortNodes(nodeset): # Generate C Code # ################### -def generateOpen62541Code(nodeset, outfilename, internal_headers=False, typesArray=[]): +def generateOpen62541NodeIds(nodeset, generate_ids_prefix, generate_ids_ns_start): + ids = StringIO() + + ids.write(u"""#ifndef UA_NODEIDS_{prefix}_H_ +#define UA_NODEIDS_{prefix}_H_ + +/** + * Namespace Zero NodeIds + * ---------------------- + * Numeric identifiers of standard-defined nodes in namespace zero. The + * following definitions are autogenerated by the nodeset compiler */ +""".format(prefix=generate_ids_prefix)) + + # get subset of nodes we should print and have numeric id + + if (sys.version_info > (3, 0)): + nodes_filtered = {k: node for k, node in nodeset.nodes.items() if node.id.ns >= generate_ids_ns_start and not node.id.i is None} + else: + nodes_filtered = {k: node for k, node in nodeset.nodes.iteritems() if node.id.ns >= generate_ids_ns_start and not node.id.i is None} + + # sort the nodes with key numeric identifier + nodes_filtered = sorted(nodes_filtered, key=lambda n: (n.ns, n.i)) + + def isNodeIdRoot(node): + # If node type is one of these types we just take the name as it is + return node.getTypeAsString() in [ "DataType", "ObjectType", "ReferenceType", "VariableType"] + + def cleanDefineName(name): + return re.sub('[=\-\.:\s]', '_', name) + + for nid in nodes_filtered: + node = nodeset.nodes[nid] + + name = cleanDefineName(node.symbolicName.value if node.symbolicName is not None else node.browseName.name) + n1 = node + # Recursively walk up through the node's parents until we reach the base node and construct the name + # along the path. Base node is determined by isNodeIdRoot + while n1 is not None: + if isNodeIdRoot(n1): + break + + parent = n1.parent + parentref = n1.parentReference + if parent is None: + # check if this node is target of a hasEncoding reference. If yes we need to add an additional "Encoding_" part to the identifier + parentreftypes = (getSubTypesOf(nodeset, nodeset.getNodeByBrowseName("HasEncoding"), [])) + parentreftypes = list(map(lambda x: x.id, parentreftypes)) + parentref = node.getParentReference(parentreftypes) + if parentref is not None: + parent = nodeset.nodes[parentref.target] + name = "Encoding_" + name + parentref = nodeset.nodes[parentref.referenceType] + + skipParents = [ NodeId("i=92"), NodeId("i=93") ] + if parent is None or parent.id.ns < generate_ids_ns_start or parent.id in skipParents: + break + # Stop on organizes references + oragnizesId = NodeId("i=35") + if parentref.id == oragnizesId: + break + + n1 = parent + name2 = cleanDefineName(parent.symbolicName.value if parent.symbolicName is not None else parent.browseName.name) + name = name2 + "_" + name + + ids.write(u"#define UA_{namespace}ID_{name} {id} /* {description} */\n".format( + namespace=generate_ids_prefix, + name=name.upper(), + id=node.id.i, + description=node.getTypeAsString())) + + ids.write(u'''#endif /* UA_NODEIDS_{0}_H_ */ \n'''.format(generate_ids_prefix)) + return ids.getvalue() + +def generateOpen62541Code( + nodeset, + outfilename, + internal_headers=False, + typesArray=[], + nodeIdsDefinitions=""): outfilebase = basename(outfilename) # Printing functions outfileh = codecs.open(outfilename + ".h", r"w+", encoding='utf-8') @@ -200,7 +280,10 @@ UA_findDataTypeByBinary(const UA_NodeId *typeId); writeh(""" _UA_BEGIN_DECLS -extern UA_StatusCode %s(UA_Server *server); +""") + writeh(nodeIdsDefinitions) + + writeh("""extern UA_StatusCode %s(UA_Server *server); _UA_END_DECLS diff --git a/tools/nodeset_compiler/nodes.py b/tools/nodeset_compiler/nodes.py index 59336023d..33d126eb2 100644 --- a/tools/nodeset_compiler/nodes.py +++ b/tools/nodeset_compiler/nodes.py @@ -185,6 +185,27 @@ class Node(object): new_refs.add(ref) self.references = new_refs + def getTypeAsString(self): + if isinstance(self, ReferenceTypeNode): + return "ReferenceType" + elif isinstance(self, ObjectNode): + return "Object" + elif isinstance(self, VariableNode) and not isinstance(self, VariableTypeNode): + return "Variable" + elif isinstance(self, VariableTypeNode): + return "VariableType" + elif isinstance(self, MethodNode): + return "Method" + elif isinstance(self, ObjectTypeNode): + return "ObjectType" + elif isinstance(self, DataTypeNode): + return "DataType" + elif isinstance(self, ViewNode): + return "View" + else: + return "Unknown" + + class ReferenceTypeNode(Node): def __init__(self, xmlelement=None): Node.__init__(self) diff --git a/tools/nodeset_compiler/nodeset_compiler.py b/tools/nodeset_compiler/nodeset_compiler.py index c269b34b1..52613fe57 100755 --- a/tools/nodeset_compiler/nodeset_compiler.py +++ b/tools/nodeset_compiler/nodeset_compiler.py @@ -42,6 +42,17 @@ parser.add_argument('--internal-headers', dest="internal_headers", help='Include internal headers instead of amalgamated header') +parser.add_argument('--generate-ids', + action='store_true', + dest="generate_ids", + default=False, + help='Generated defines for node ids in header file for nodesets of the --xml parameter. Default True. This will genera') + +parser.add_argument('--ids-prefix', + dest="generate_ids_prefix", + default="NS", + help='Prefix for generated node id defines. Format is UA_{PREFIX}ID_{name}.') + parser.add_argument('-b', '--blacklist', metavar="", type=argparse.FileType('r'), @@ -122,6 +133,10 @@ for xmlfile in args.existing: logger.info("Preprocessing (existing) " + str(xmlfile.name)) ns.addNodeSet(xmlfile, True, typesArray=getTypesArray(nsCount)) nsCount +=1 + +# We only generate nodeids for the nodesets passed as the --xml parameter +nodeIdsNamespaceIndexToGenerate = nsCount + for xmlfile in args.infiles: if xmlfile.name in loadedFiles: logger.info("Skipping Nodeset since it is already loaded: {} ".format(xmlfile.name)) @@ -131,10 +146,6 @@ for xmlfile in args.infiles: ns.addNodeSet(xmlfile, typesArray=getTypesArray(nsCount)) nsCount +=1 -# # We need to notify the open62541 server of the namespaces used to be able to use i.e. ns=3 -# namespaceArrayNames = preProc.getUsedNamespaceArrayNames() -# for key in namespaceArrayNames: -# ns.addNamespace(key, namespaceArrayNames[key]) # Set the nodes from the ignore list to hidden. This removes them from dependency calculation # and from printing their generated code. @@ -185,8 +196,9 @@ logger.info("Generating Code for Backend: {}".format(args.backend)) if args.backend == "open62541": # Create the C code with the open62541 backend of the compiler - from backend_open62541 import generateOpen62541Code - generateOpen62541Code(ns, args.outputFile, args.internal_headers, args.typesArray) + from backend_open62541 import generateOpen62541Code, generateOpen62541NodeIds + nodeIdsDefinitions = generateOpen62541NodeIds(ns, args.generate_ids_prefix, nodeIdsNamespaceIndexToGenerate) + generateOpen62541Code(ns, args.outputFile, args.internal_headers, args.typesArray, nodeIdsDefinitions) elif args.backend == "graphviz": from backend_graphviz import generateGraphvizCode generateGraphvizCode(ns, filename=args.outputFile)