#!/usr/bin/env python # -*- coding: utf-8 -*- ### This Source Code Form is subject to the terms of the Mozilla Public ### License, v. 2.0. If a copy of the MPL was not distributed with this ### file, You can obtain one at http://mozilla.org/MPL/2.0/. ### Copyright 2014-2015 (c) TU-Dresden (Author: Chris Iatrou) ### Copyright 2014-2017 (c) Fraunhofer IOSB (Author: Julius Pfrommer) ### Copyright 2016-2017 (c) Stefan Profanter, fortiss GmbH ### Copyright 2019 (c) Andrea Minosu ### Copyright 2018 (c) Jannis Volker ### Copyright 2018 (c) Ralph Lange from nodes import * from backend_open62541_datatypes import * import re import logging import sys if sys.version_info[0] >= 3: # strings are already parsed to unicode def unicode(s): return s logger = logging.getLogger(__name__) ################# # Generate Code # ################# def generateNodeIdPrintable(node): if isinstance(node.id, NodeId): CodePrintable = node.__class__.__name__ + "_" + str(node.id) else: CodePrintable = node.__class__.__name__ + "_unknown_nid" return re.sub('[^0-9a-z_]+', '_', CodePrintable.lower()) def generateNodeValueInstanceName(node, parent, arrayIndex): return generateNodeIdPrintable(parent) + "_" + str(node.alias) + "_" + str(arrayIndex) def generateReferenceCode(reference): if reference.isForward: return "retVal |= UA_Server_addReference(server, %s, %s, %s, true);" % \ (generateNodeIdCode(reference.source), generateNodeIdCode(reference.referenceType), generateExpandedNodeIdCode(reference.target)) else: return "retVal |= UA_Server_addReference(server, %s, %s, %s, false);" % \ (generateNodeIdCode(reference.source), generateNodeIdCode(reference.referenceType), generateExpandedNodeIdCode(reference.target)) def generateReferenceTypeNodeCode(node): code = [] code.append("UA_ReferenceTypeAttributes attr = UA_ReferenceTypeAttributes_default;") if node.isAbstract: code.append("attr.isAbstract = true;") if node.symmetric: code.append("attr.symmetric = true;") if node.inverseName != "": code.append("attr.inverseName = UA_LOCALIZEDTEXT(\"\", \"%s\");" % \ node.inverseName) return code def generateObjectNodeCode(node): code = [] code.append("UA_ObjectAttributes attr = UA_ObjectAttributes_default;") if node.eventNotifier: code.append("attr.eventNotifier = true;") return code def setNodeDatatypeRecursive(node, nodeset): if not isinstance(node, VariableNode) and not isinstance(node, VariableTypeNode): raise RuntimeError("Node {}: DataType can only be set for VariableNode and VariableTypeNode".format(str(node.id))) if node.dataType is not None: return # If BaseVariableType if node.id == NodeId("ns=0;i=62"): if node.dataType is None: # Set to default BaseDataType node.dataType = NodeId("ns=0;i=24") return if isinstance(node, VariableNode) and not isinstance(node, VariableTypeNode): typeDefNode = nodeset.getNodeTypeDefinition(node) if typeDefNode is None: # Use the parent type. raise RuntimeError("Cannot get node for HasTypeDefinition of VariableNode " + node.browseName.name + " " + str(node.id)) setNodeDatatypeRecursive(typeDefNode, nodeset) node.dataType = typeDefNode.dataType else: # Use the parent type. if node.parent is None: raise RuntimeError("Parent node not defined for " + node.browseName.name + " " + str(node.id)) setNodeDatatypeRecursive(node.parent, nodeset) node.dataType = node.parent.dataType def setNodeValueRankRecursive(node, nodeset): if not isinstance(node, VariableNode) and not isinstance(node, VariableTypeNode): raise RuntimeError("Node {}: ValueRank can only be set for VariableNode and VariableTypeNode".format(str(node.id))) if node.valueRank is not None: return # If BaseVariableType if node.id == NodeId("ns=0;i=62"): if node.valueRank is None: # BaseVariableType always has -2 node.valueRank = -2 return if isinstance(node, VariableNode) and not isinstance(node, VariableTypeNode): typeDefNode = nodeset.getNodeTypeDefinition(node) if typeDefNode is None: # Use the parent type. raise RuntimeError("Cannot get node for HasTypeDefinition of VariableNode " + node.browseName.name + " " + str(node.id)) if not isinstance(typeDefNode, VariableTypeNode): raise RuntimeError("Node {} ({}) has an invalid type definition. {} is not a VariableType node.".format( str(node.id), node.browseName.name, str(typeDefNode.id))) setNodeValueRankRecursive(typeDefNode, nodeset) if typeDefNode.valueRank is not None and typeDefNode.valueRank > -1: node.valueRank = typeDefNode.valueRank else: # Default value node.valueRank = -1 else: # Check if parent node limits the value rank setNodeValueRankRecursive(node.parent, nodeset) if node.parent.valueRank is not None and node.parent.valueRank > -1: node.valueRank = node.parent.valueRank else: # Default value node.valueRank = -1 def generateCommonVariableCode(node, nodeset): code = [] codeCleanup = [] codeGlobal = [] if node.valueRank is None: # Set the constrained value rank from the type/parent node setNodeValueRankRecursive(node, nodeset) code.append("/* Value rank inherited */") code.append("attr.valueRank = %d;" % node.valueRank) if node.valueRank > 0: code.append("attr.arrayDimensionsSize = %d;" % node.valueRank) code.append("UA_UInt32 arrayDimensions[{}];".format(node.valueRank)) if len(node.arrayDimensions) == node.valueRank: for idx, v in enumerate(node.arrayDimensions): code.append("arrayDimensions[{}] = {};".format(idx, int(str(v)))) else: for dim in range(0, node.valueRank): code.append("arrayDimensions[{}] = 0;".format(dim)) code.append("attr.arrayDimensions = &arrayDimensions[0];") if node.dataType is None: # Inherit the datatype from the HasTypeDefinition reference, as stated in the OPC UA Spec: # 6.4.2 # "Instances inherit the initial values for the Attributes that they have in common with the # TypeDefinitionNode from which they are instantiated, with the exceptions of the NodeClass and # NodeId." setNodeDatatypeRecursive(node, nodeset) code.append("/* DataType inherited */") dataTypeNode = nodeset.getBaseDataType(nodeset.getDataTypeNode(node.dataType)) if dataTypeNode is None: raise RuntimeError("Cannot get BaseDataType for dataType : " + str(node.dataType) + " of node " + node.browseName.name + " " + str(node.id)) code.append("attr.dataType = %s;" % generateNodeIdCode(node.dataType)) if dataTypeNode.isEncodable(): if node.value is not None: [code1, codeCleanup1, codeGlobal1] = generateValueCode(node.value, nodeset.nodes[node.id], nodeset) code += code1 codeCleanup += codeCleanup1 codeGlobal += codeGlobal1 # #1978 Variant arrayDimensions are only required to properly decode multidimensional arrays # (valueRank > 1) from data stored as one-dimensional array of arrayLength elements. # One-dimensional arrays are already completely defined by arraylength attribute so setting # also arrayDimensions, even if not explicitly forbidden, can confuse clients if node.valueRank is not None and node.valueRank > 1 and len(node.arrayDimensions) == node.valueRank and len(node.value.value) > 0: numElements = 1 hasZero = False for v in node.arrayDimensions: dim = int(unicode(v)) if dim > 0: numElements = numElements * dim else: hasZero = True if hasZero == False and len(node.value.value) == numElements: code.append("attr.value.arrayDimensionsSize = attr.arrayDimensionsSize;") code.append("attr.value.arrayDimensions = attr.arrayDimensions;") elif node.value is not None: code.append("/* Cannot encode the value */") logger.warn("Cannot encode dataTypeNode: " + dataTypeNode.browseName.name + " for value of node " + node.browseName.name + " " + str(node.id)) return [code, codeCleanup, codeGlobal] def generateVariableNodeCode(node, nodeset): code = [] codeCleanup = [] codeGlobal = [] code.append("UA_VariableAttributes attr = UA_VariableAttributes_default;") if node.historizing: code.append("attr.historizing = true;") code.append("attr.minimumSamplingInterval = %f;" % node.minimumSamplingInterval) code.append("attr.userAccessLevel = %d;" % node.userAccessLevel) code.append("attr.accessLevel = %d;" % node.accessLevel) # in order to be compatible with mostly OPC UA client # force valueRank = -1 for scalar VariableNode if node.valueRank == -2 and node.value is not None and len(node.value.value) == 1: node.valueRank = -1 [code1, codeCleanup1, codeGlobal1] = generateCommonVariableCode(node, nodeset) code += code1 codeCleanup += codeCleanup1 codeGlobal += codeGlobal1 return [code, codeCleanup, codeGlobal] def generateVariableTypeNodeCode(node, nodeset): code = [] codeCleanup = [] codeGlobal = [] code.append("UA_VariableTypeAttributes attr = UA_VariableTypeAttributes_default;") if node.isAbstract: code.append("attr.isAbstract = true;") [code1, codeCleanup1, codeGlobal1] = generateCommonVariableCode(node, nodeset) code += code1 codeCleanup += codeCleanup1 codeGlobal += codeGlobal1 return [code, codeCleanup, codeGlobal] def lowerFirstChar(inputString): return inputString[0].lower() + inputString[1:] def generateExtensionObjectSubtypeCode(node, parent, nodeset, global_var_code, instanceName=None, isArrayElement=False): code = [""] codeCleanup = [""] logger.debug("Building extensionObject for " + str(parent.id)) logger.debug("Encoding " + str(node.encodingRule)) typeBrowseNode = makeCIdentifier(nodeset.getDataTypeNode(parent.dataType).browseName.name) #TODO: review this if typeBrowseNode == "NumericRange": # in the stack we define a separate structure for the numeric range, but # the value itself is just a string typeBrowseNode = "String" typeString = "UA_" + typeBrowseNode if instanceName is None: instanceName = generateNodeValueInstanceName(node, parent, 0) code.append("UA_STACKARRAY(" + typeString + ", " + instanceName + ", 1);") typeArr = nodeset.getDataTypeNode(parent.dataType).typesArray typeString = nodeset.getDataTypeNode(parent.dataType).browseName.name.upper() typeArrayString = typeArr + "[" + typeArr + "_" + typeString + "]" code.append("UA_init({ref}{instanceName}, &{typeArrayString});".format(ref="&" if isArrayElement else "", instanceName=instanceName, typeArrayString=typeArrayString)) # Assign data to the struct contents # Track the encoding rule definition to detect arrays and/or ExtensionObjects values = node.value if values == None: values = [] for idx,subv in enumerate(values): encField = node.encodingRule[idx] memberName = lowerFirstChar(encField[0]) # Check if this is an array accessor = "." if isArrayElement else "->" if isinstance(subv, list): if len(subv) == 0: continue logger.info("ExtensionObject contains array") memberName = lowerFirstChar(encField[0]) encTypeString = "UA_" + subv[0].__class__.__name__ instanceNameSafe = makeCIdentifier(instanceName) code.append("UA_STACKARRAY(" + encTypeString + ", " + instanceNameSafe + "_" + memberName+", {0});".format(len(subv))) encTypeArr = nodeset.getDataTypeNode(subv[0].__class__.__name__).typesArray encTypeArrayString = encTypeArr + "[" + encTypeArr + "_" + subv[0].__class__.__name__.upper() + "]" code.append("UA_init({instanceName}, &{typeArrayString});".format(instanceName=instanceNameSafe + "_" + memberName, typeArrayString=encTypeArrayString)) for subArrayIdx,val in enumerate(subv): code.append(generateNodeValueCode(instanceNameSafe + "_" + memberName + "[" + str(subArrayIdx) + "]" +" = ", val, instanceName,instanceName + "_gehtNed_member", global_var_code, asIndirect=False)) code.append(instanceName + accessor + memberName + "Size = {0};".format(len(subv))) code.append(instanceName + accessor + memberName + " = " + instanceNameSafe+"_"+ memberName+";") continue logger.debug("Encoding of field " + memberName + " is " + str(subv.encodingRule) + "defined by " + str(encField)) if subv.valueRank is None or subv.valueRank == 0: if not subv.isNone(): # Some values can be optional valueName = instanceName + accessor + memberName code.append(generateNodeValueCode(valueName + " = " , subv, instanceName,valueName, global_var_code, asIndirect=False)) else: memberName = lowerFirstChar(encField[0]) code.append(generateNodeValueCode(instanceName + accessor + memberName + "Size = ", subv, instanceName,valueName, global_var_code, asIndirect=False)) if not isArrayElement: code.append("UA_Variant_setScalar(&attr.value, " + instanceName + ", &" + typeArrayString + ");") return [code, codeCleanup] def getTypeBrowseName(dataTypeNode): typeBrowseName = makeCIdentifier(dataTypeNode.browseName.name) #TODO: review this if typeBrowseName == "NumericRange": # in the stack we define a separate structure for the numeric range, but # the value itself is just a string typeBrowseName = "String" return typeBrowseName def getTypesArrayForValue(nodeset, value): typeNode = nodeset.getNodeByBrowseName(value.__class__.__name__) if typeNode is None or value.isInternal: typesArray = "UA_TYPES" else: typesArray = typeNode.typesArray typeName = makeCIdentifier(value.__class__.__name__.upper()) return "&" + typesArray + "[" + typesArray + "_" + typeName + "]" def isArrayVariableNode(node, parentNode): return parentNode.valueRank is not None and (parentNode.valueRank != -1 and (parentNode.valueRank >= 0 or (len(node.value) > 1 and (parentNode.valueRank != -2 or parentNode.valueRank != -3)))) def generateValueCode(node, parentNode, nodeset, bootstrapping=True): code = [] codeCleanup = [] codeGlobal = [] valueName = generateNodeIdPrintable(parentNode) + "_variant_DataContents" # node.value either contains a list of multiple identical BUILTINTYPES, or it # contains a single builtintype (which may be a container); choose if we need # to create an array or a single variable. # Note that some genious defined that there are arrays of size 1, which are # distinctly different then a single value, so we need to check that as well # Semantics: # -3: Scalar or 1-dim # -2: Scalar or x-dim | x>0 # -1: Scalar # 0: x-dim | x>0 # n: n-dim | n>0 if (len(node.value) == 0): return ["", "", ""] if not isinstance(node.value[0], Value): return ["", "", ""] dataTypeNode = nodeset.getDataTypeNode(parentNode.dataType) if isArrayVariableNode(node, parentNode): # User the following strategy for all directly mappable values a la 'UA_Type MyInt = (UA_Type) 23;' if isinstance(node.value[0], Guid): logger.warn("Don't know how to print array of GUID in node " + str(parentNode.id)) elif isinstance(node.value[0], DiagnosticInfo): logger.warn("Don't know how to print array of DiagnosticInfo in node " + str(parentNode.id)) elif isinstance(node.value[0], StatusCode): logger.warn("Don't know how to print array of StatusCode in node " + str(parentNode.id)) else: if isinstance(node.value[0], ExtensionObject): code.append("UA_" + getTypeBrowseName(dataTypeNode) + " " + valueName + "[" + str(len(node.value)) + "];") for idx, v in enumerate(node.value): logger.debug("Building extObj array index " + str(idx)) instanceName = valueName + "[" + str(idx) + "]" [code1, codeCleanup1] = generateExtensionObjectSubtypeCode(v, parent=parentNode, nodeset=nodeset, global_var_code=codeGlobal, instanceName=instanceName, isArrayElement=True) code = code + code1 codeCleanup = codeCleanup + codeCleanup1 else: code.append("UA_" + node.value[0].__class__.__name__ + " " + valueName + "[" + str(len(node.value)) + "];") for idx, v in enumerate(node.value): instanceName = generateNodeValueInstanceName(v, parentNode, idx) code.append(generateNodeValueCode( valueName + "[" + str(idx) + "] = " , v, instanceName, valueName, codeGlobal)) code.append("UA_Variant_setArray(&attr.value, &" + valueName + ", (UA_Int32) " + str(len(node.value)) + ", " + "&" + dataTypeNode.typesArray + "["+dataTypeNode.typesArray + "_" + getTypeBrowseName(dataTypeNode).upper() +"]);") #scalar value else: # User the following strategy for all directly mappable values a la 'UA_Type MyInt = (UA_Type) 23;' if isinstance(node.value[0], Guid): logger.warn("Don't know how to print scalar GUID in node " + str(parentNode.id)) elif isinstance(node.value[0], DiagnosticInfo): logger.warn("Don't know how to print scalar DiagnosticInfo in node " + str(parentNode.id)) elif isinstance(node.value[0], StatusCode): logger.warn("Don't know how to print scalar StatusCode in node " + str(parentNode.id)) else: # The following strategy applies to all other types, in particular strings and numerics. if isinstance(node.value[0], ExtensionObject): [code1, codeCleanup1] = generateExtensionObjectSubtypeCode(node.value[0], parent=parentNode, nodeset=nodeset, global_var_code=codeGlobal, isArrayElement=False) code = code + code1 codeCleanup = codeCleanup + codeCleanup1 instanceName = generateNodeValueInstanceName(node.value[0], parentNode, 0) if not node.value[0].isNone() and not(isinstance(node.value[0], ExtensionObject)): code.append("UA_" + node.value[0].__class__.__name__ + " *" + valueName + " = UA_" + node.value[ 0].__class__.__name__ + "_new();") code.append("if (!" + valueName + ") return UA_STATUSCODE_BADOUTOFMEMORY;") code.append("UA_" + node.value[0].__class__.__name__ + "_init(" + valueName + ");") code.append(generateNodeValueCode("*" + valueName + " = " , node.value[0], instanceName, valueName, codeGlobal, asIndirect=True)) code.append( "UA_Variant_setScalar(&attr.value, " + valueName + ", " + getTypesArrayForValue(nodeset, node.value[0]) + ");") if node.value[0].__class__.__name__ == "ByteString": # The data is on the stack, not heap, so we can not delete the ByteString codeCleanup.append("{}->data = NULL;".format(valueName)) codeCleanup.append("{}->length = 0;".format(valueName)) codeCleanup.append("UA_{0}_delete({1});".format( node.value[0].__class__.__name__, valueName)) return [code, codeCleanup, codeGlobal] def generateMethodNodeCode(node): code = [] code.append("UA_MethodAttributes attr = UA_MethodAttributes_default;") if node.executable: code.append("attr.executable = true;") if node.userExecutable: code.append("attr.userExecutable = true;") return code def generateObjectTypeNodeCode(node): code = [] code.append("UA_ObjectTypeAttributes attr = UA_ObjectTypeAttributes_default;") if node.isAbstract: code.append("attr.isAbstract = true;") return code def generateDataTypeNodeCode(node): code = [] code.append("UA_DataTypeAttributes attr = UA_DataTypeAttributes_default;") if node.isAbstract: code.append("attr.isAbstract = true;") return code def generateViewNodeCode(node): code = [] code.append("UA_ViewAttributes attr = UA_ViewAttributes_default;") if node.containsNoLoops: code.append("attr.containsNoLoops = true;") code.append("attr.eventNotifier = (UA_Byte)%s;" % str(node.eventNotifier)) return code def generateSubtypeOfDefinitionCode(node): for ref in node.inverseReferences: # 45 = HasSubtype if ref.referenceType.i == 45: return generateNodeIdCode(ref.target) return "UA_NODEID_NULL" def generateNodeCode_begin(node, nodeset, code_global): code = [] codeCleanup = [] code.append("UA_StatusCode retVal = UA_STATUSCODE_GOOD;") # Attributes if isinstance(node, ReferenceTypeNode): code.extend(generateReferenceTypeNodeCode(node)) elif isinstance(node, ObjectNode): code.extend(generateObjectNodeCode(node)) elif isinstance(node, VariableNode) and not isinstance(node, VariableTypeNode): [code1, codeCleanup1, codeGlobal1] = generateVariableNodeCode(node, nodeset) code.extend(code1) codeCleanup.extend(codeCleanup1) code_global.extend(codeGlobal1) elif isinstance(node, VariableTypeNode): [code1, codeCleanup1, codeGlobal1] = generateVariableTypeNodeCode(node, nodeset) code.extend(code1) codeCleanup.extend(codeCleanup1) code_global.extend(codeGlobal1) elif isinstance(node, MethodNode): code.extend(generateMethodNodeCode(node)) elif isinstance(node, ObjectTypeNode): code.extend(generateObjectTypeNodeCode(node)) elif isinstance(node, DataTypeNode): code.extend(generateDataTypeNodeCode(node)) elif isinstance(node, ViewNode): code.extend(generateViewNodeCode(node)) if node.displayName is not None: code.append("attr.displayName = " + generateLocalizedTextCode(node.displayName, alloc=False) + ";") if node.description is not None: code.append("#ifdef UA_ENABLE_NODESET_COMPILER_DESCRIPTIONS") code.append("attr.description = " + generateLocalizedTextCode(node.description, alloc=False) + ";") code.append("#endif") if node.writeMask is not None: code.append("attr.writeMask = %d;" % node.writeMask) if node.userWriteMask is not None: code.append("attr.userWriteMask = %d;" % node.userWriteMask) # AddNodes call code.append("retVal |= UA_Server_addNode_begin(server, UA_NODECLASS_{},". format(makeCIdentifier(node.__class__.__name__.upper().replace("NODE" ,"")))) code.append(generateNodeIdCode(node.id) + ",") code.append(generateNodeIdCode(node.parent.id if node.parent else NodeId()) + ",") code.append(generateNodeIdCode(node.parentReference.id if node.parent else NodeId()) + ",") code.append(generateQualifiedNameCode(node.browseName) + ",") if isinstance(node, VariableNode) or isinstance(node, ObjectNode): typeDefRef = node.popTypeDef() code.append(generateNodeIdCode(typeDefRef.target) + ",") else: code.append(" UA_NODEID_NULL,") code.append("(const UA_NodeAttributes*)&attr, &UA_TYPES[UA_TYPES_{}ATTRIBUTES],NULL, NULL);". format(makeCIdentifier(node.__class__.__name__.upper().replace("NODE" ,"")))) code.extend(codeCleanup) return "\n".join(code) def generateNodeCode_finish(node): code = [] if isinstance(node, MethodNode): code.append("UA_Server_addMethodNode_finish(server, ") else: code.append("UA_Server_addNode_finish(server, ") code.append(generateNodeIdCode(node.id)) if isinstance(node, MethodNode): code.append(", NULL, 0, NULL, 0, NULL);") else: code.append(");") return "\n".join(code)