ua_nodeset.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # This Source Code Form is subject to the terms of the Mozilla Public
  4. # License, v. 2.0. If a copy of the MPL was not distributed with this
  5. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. ###
  7. ### Author: Chris Iatrou (ichrispa@core-vector.net)
  8. ### Version: rev 13
  9. ###
  10. ### This program was created for educational purposes and has been
  11. ### contributed to the open62541 project by the author. All licensing
  12. ### terms for this source is inherited by the terms and conditions
  13. ### specified for by the open62541 project (see the projects readme
  14. ### file for more information on the MPLv2 terms and restrictions).
  15. ###
  16. ### This program is not meant to be used in a production environment. The
  17. ### author is not liable for any complications arising due to the use of
  18. ### this program.
  19. ###
  20. from __future__ import print_function
  21. import sys
  22. from struct import pack as structpack
  23. from collections import deque
  24. from time import struct_time, strftime, strptime, mktime
  25. import logging; logger = logging.getLogger(__name__)
  26. from ua_builtin_types import *;
  27. from ua_node_types import *;
  28. from ua_constants import *;
  29. def getNextElementNode(xmlvalue):
  30. if xmlvalue == None:
  31. return None
  32. xmlvalue = xmlvalue.nextSibling
  33. while not xmlvalue == None and not xmlvalue.nodeType == xmlvalue.ELEMENT_NODE:
  34. xmlvalue = xmlvalue.nextSibling
  35. return xmlvalue
  36. class NodeSet():
  37. """ Class holding and managing a set of OPCUA nodes.
  38. This class handles parsing XML description of namespaces, instantiating
  39. nodes, linking references, graphing the namespace and compiling a binary
  40. representation.
  41. Note that nodes assigned to this class are not restricted to having a
  42. single namespace ID. This class represents the entire physical address
  43. space of the binary representation and all nodes that are to be included
  44. in that segment of memory.
  45. """
  46. nodes = []
  47. nodeids = {}
  48. aliases = {}
  49. __linkLater__ = []
  50. __binaryIndirectPointers__ = []
  51. name = ""
  52. knownNodeTypes = ""
  53. namespaceIdentifiers = {} # list of 'int':'string' giving different namespace an array-mapable name
  54. def __init__(self, name):
  55. self.nodes = []
  56. self.knownNodeTypes = ['variable', 'object', 'method', 'referencetype', \
  57. 'objecttype', 'variabletype', 'methodtype', \
  58. 'datatype', 'referencetype', 'aliases']
  59. self.name = name
  60. self.nodeids = {}
  61. self.aliases = {}
  62. self.namespaceIdentifiers = {}
  63. self.__binaryIndirectPointers__ = []
  64. def addNamespace(self, numericId, stringURL):
  65. self.namespaceIdentifiers[numericId] = stringURL
  66. def linkLater(self, pointer):
  67. """ Called by nodes or references who have parsed an XML reference to a
  68. node represented by a string.
  69. No return value
  70. XML String representations of references have the form 'i=xy' or
  71. 'ns=1;s="This unique Node"'. Since during the parsing of this attribute
  72. only a subset of nodes are known/parsed, this reference string cannot be
  73. linked when encountered.
  74. References register themselves with the namespace to have their target
  75. attribute (string) parsed by linkOpenPointers() when all nodes are
  76. created, so that target can be dereferenced an point to an actual node.
  77. """
  78. self.__linkLater__.append(pointer)
  79. def getUnlinkedPointers(self):
  80. """ Return the list of references registered for linking during the next call
  81. of linkOpenPointers()
  82. """
  83. return self.__linkLater__
  84. def unlinkedItemCount(self):
  85. """ Returns the number of unlinked references that will be processed during
  86. the next call of linkOpenPointers()
  87. """
  88. return len(self.__linkLater__)
  89. def buildAliasList(self, xmlelement):
  90. """ Parses the <Alias> XML Element present in must XML NodeSet definitions.
  91. No return value
  92. Contents the Alias element are stored in a dictionary for further
  93. dereferencing during pointer linkage (see linkOpenPointer()).
  94. """
  95. if not xmlelement.tagName == "Aliases":
  96. logger.error("XMLElement passed is not an Aliaslist")
  97. return
  98. for al in xmlelement.childNodes:
  99. if al.nodeType == al.ELEMENT_NODE:
  100. if al.hasAttribute("Alias"):
  101. aliasst = al.getAttribute("Alias")
  102. if sys.version_info[0] < 3:
  103. aliasnd = unicode(al.firstChild.data)
  104. else:
  105. aliasnd = al.firstChild.data
  106. if not aliasst in self.aliases:
  107. self.aliases[aliasst] = aliasnd
  108. logger.debug("Added new alias \"" + str(aliasst) + "\" == \"" + str(aliasnd) + "\"")
  109. else:
  110. if self.aliases[aliasst] != aliasnd:
  111. logger.error("Alias definitions for " + aliasst + " differ. Have " + self.aliases[aliasst] + " but XML defines " + aliasnd + ". Keeping current definition.")
  112. def getNodeByBrowseName(self, idstring):
  113. """ Returns the first node in the nodelist whose browseName matches idstring.
  114. """
  115. return next((n for n in self.nodes if idstring==str(n.browseName())), None)
  116. def getNodeByIDString(self, idstring):
  117. """ Returns the first node in the nodelist whose id string representation
  118. matches idstring.
  119. """
  120. return next((n for n in self.nodes if idstring==str(n.id())), None)
  121. def createNode(self, ndtype, xmlelement):
  122. """ createNode is instantiates a node described by xmlelement, its type being
  123. defined by the string ndtype.
  124. No return value
  125. If the xmlelement is an <Alias>, the contents will be parsed and stored
  126. for later dereferencing during pointer linking (see linkOpenPointers).
  127. Recognized types are:
  128. * UAVariable
  129. * UAObject
  130. * UAMethod
  131. * UAView
  132. * UAVariableType
  133. * UAObjectType
  134. * UAMethodType
  135. * UAReferenceType
  136. * UADataType
  137. For every recognized type, an appropriate node class is added to the node
  138. list of the namespace. The NodeId of the given node is created and parsing
  139. of the node attributes and elements is delegated to the parseXML() and
  140. parseXMLSubType() functions of the instantiated class.
  141. If the NodeID attribute is non-unique in the node list, the creation is
  142. deferred and an error is logged.
  143. """
  144. if not isinstance(xmlelement, dom.Element):
  145. logger.error( "Error: Can not create node from invalid XMLElement")
  146. return
  147. # An ID is mandatory for everything but aliases!
  148. id = None
  149. for idname in ['NodeId', 'NodeID', 'nodeid']:
  150. if xmlelement.hasAttribute(idname):
  151. id = xmlelement.getAttribute(idname)
  152. if ndtype == 'aliases':
  153. self.buildAliasList(xmlelement)
  154. return
  155. elif id == None:
  156. logger.info( "Error: XMLElement has no id, node will not be created!")
  157. return
  158. else:
  159. id = opcua_node_id_t(id)
  160. if str(id) in self.nodeids:
  161. # Normal behavior: Do not allow duplicates, first one wins
  162. #logger.error( "XMLElement with duplicate ID " + str(id) + " found, node will not be created!")
  163. #return
  164. # Open62541 behavior for header generation: Replace the duplicate with the new node
  165. logger.info( "XMLElement with duplicate ID " + str(id) + " found, node will be replaced!")
  166. nd = self.getNodeByIDString(str(id))
  167. self.nodes.remove(nd)
  168. self.nodeids.pop(str(nd.id()))
  169. node = None
  170. if (ndtype == 'variable'):
  171. node = opcua_node_variable_t(id, self)
  172. elif (ndtype == 'object'):
  173. node = opcua_node_object_t(id, self)
  174. elif (ndtype == 'method'):
  175. node = opcua_node_method_t(id, self)
  176. elif (ndtype == 'objecttype'):
  177. node = opcua_node_objectType_t(id, self)
  178. elif (ndtype == 'variabletype'):
  179. node = opcua_node_variableType_t(id, self)
  180. elif (ndtype == 'methodtype'):
  181. node = opcua_node_methodType_t(id, self)
  182. elif (ndtype == 'datatype'):
  183. node = opcua_node_dataType_t(id, self)
  184. elif (ndtype == 'referencetype'):
  185. node = opcua_node_referenceType_t(id, self)
  186. else:
  187. logger.error( "No node constructor for type " + ndtype)
  188. if node != None:
  189. node.parseXML(xmlelement)
  190. self.nodes.append(node)
  191. self.nodeids[str(node.id())] = node
  192. def removeNodeById(self, nodeId):
  193. nd = self.getNodeByIDString(nodeId)
  194. if nd == None:
  195. return False
  196. logger.debug("Removing nodeId " + str(nodeId))
  197. self.nodes.remove(nd)
  198. if nd.getInverseReferences() != None:
  199. for ref in nd.getInverseReferences():
  200. src = ref.target();
  201. src.removeReferenceToNode(nd)
  202. return True
  203. def registerBinaryIndirectPointer(self, node):
  204. """ Appends a node to the list of nodes that should be contained in the
  205. first 765 bytes (255 pointer slots a 3 bytes) in the binary
  206. representation (indirect referencing space).
  207. This function is reserved for references and dataType pointers.
  208. """
  209. if not node in self.__binaryIndirectPointers__:
  210. self.__binaryIndirectPointers__.append(node)
  211. return self.__binaryIndirectPointers__.index(node)
  212. def getBinaryIndirectPointerIndex(self, node):
  213. """ Returns the slot/index of a pointer in the indirect referencing space
  214. (first 765 Bytes) of the binary representation.
  215. """
  216. if not node in self.__binaryIndirectPointers__:
  217. return -1
  218. return self.__binaryIndirectPointers__.index(node)
  219. def parseXML(self, xmldoc):
  220. """ Reads an XML Namespace definition and instantiates node.
  221. No return value
  222. parseXML open the file xmldoc using xml.dom.minidom and searches for
  223. the first UANodeSet Element. For every Element encountered, createNode
  224. is called to instantiate a node of the appropriate type.
  225. """
  226. typedict = {}
  227. UANodeSet = dom.parse(xmldoc).getElementsByTagName("UANodeSet")
  228. if len(UANodeSet) == 0:
  229. logger.error( "Error: No NodeSets found")
  230. return
  231. if len(UANodeSet) != 1:
  232. logger.error( "Error: Found more than 1 Nodeset in XML File")
  233. UANodeSet = UANodeSet[0]
  234. for nd in UANodeSet.childNodes:
  235. if nd.nodeType != nd.ELEMENT_NODE:
  236. continue
  237. ndType = nd.tagName.lower()
  238. if ndType[:2] == "ua":
  239. ndType = ndType[2:]
  240. elif not ndType in self.knownNodeTypes:
  241. logger.warn("XML Element or NodeType " + ndType + " is unknown and will be ignored")
  242. continue
  243. if not ndType in typedict:
  244. typedict[ndType] = 1
  245. else:
  246. typedict[ndType] = typedict[ndType] + 1
  247. self.createNode(ndType, nd)
  248. logger.debug("Currently " + str(len(self.nodes)) + " nodes in address space. Type distribution for this run was: " + str(typedict))
  249. def linkOpenPointers(self):
  250. """ Substitutes symbolic NodeIds in references for actual node instances.
  251. No return value
  252. References that have registered themselves with linkLater() to have
  253. their symbolic NodeId targets ("ns=2;i=32") substituted for an actual
  254. node will be iterated by this function. For each reference encountered
  255. in the list of unlinked/open references, the target string will be
  256. evaluated and searched for in the node list of this namespace. If found,
  257. the target attribute of the reference will be substituted for the
  258. found node.
  259. If a reference fails to get linked, it will remain in the list of
  260. unlinked references. The individual items in this list can be
  261. retrieved using getUnlinkedPointers().
  262. """
  263. linked = []
  264. logger.debug( str(self.unlinkedItemCount()) + " pointers need to get linked.")
  265. for l in self.__linkLater__:
  266. targetLinked = False
  267. if not l.target() == None and not isinstance(l.target(), opcua_node_t):
  268. if isinstance(l.target(),str) or isinstance(l.target(),unicode):
  269. # If is not a node ID, it should be an alias. Try replacing it
  270. # with a proper node ID
  271. if l.target() in self.aliases:
  272. l.target(self.aliases[l.target()])
  273. # If the link is a node ID, try to find it hopening that no ass has
  274. # defined more than one kind of id for that sucker
  275. if l.target()[:2] == "i=" or l.target()[:2] == "g=" or \
  276. l.target()[:2] == "b=" or l.target()[:2] == "s=" or \
  277. l.target()[:3] == "ns=" :
  278. tgt = self.getNodeByIDString(str(l.target()))
  279. if tgt == None:
  280. logger.error("Failed to link pointer to target (node not found) " + l.target())
  281. else:
  282. l.target(tgt)
  283. targetLinked = True
  284. else:
  285. logger.error("Failed to link pointer to target (target not Alias or Node) " + l.target())
  286. else:
  287. logger.error("Failed to link pointer to target (don't know dummy type + " + str(type(l.target())) + " +) " + str(l.target()))
  288. else:
  289. logger.error("Pointer has null target: " + str(l))
  290. referenceLinked = False
  291. if not l.referenceType() == None:
  292. if l.referenceType() in self.aliases:
  293. l.referenceType(self.aliases[l.referenceType()])
  294. tgt = self.getNodeByIDString(str(l.referenceType()))
  295. if tgt == None:
  296. logger.error("Failed to link reference type to target (node not found) " + l.referenceType())
  297. else:
  298. l.referenceType(tgt)
  299. referenceLinked = True
  300. else:
  301. referenceLinked = True
  302. if referenceLinked == True and targetLinked == True:
  303. linked.append(l)
  304. # References marked as "not forward" must be inverted (removed from source
  305. # node, assigned to target node and relinked)
  306. logger.warn("Inverting reference direction for all references with isForward==False attribute (is this correct!?)")
  307. for n in self.nodes:
  308. for r in n.getReferences():
  309. if r.isForward() == False:
  310. tgt = r.target()
  311. if isinstance(tgt, opcua_node_t):
  312. nref = opcua_referencePointer_t(n, parentNode=tgt)
  313. nref.referenceType(r.referenceType())
  314. tgt.addReference(nref)
  315. # Create inverse references for all nodes
  316. logger.debug("Updating all referencedBy fields in nodes for inverse lookups.")
  317. for n in self.nodes:
  318. n.updateInverseReferences()
  319. for l in linked:
  320. self.__linkLater__.remove(l)
  321. if len(self.__linkLater__) != 0:
  322. logger.warn(str(len(self.__linkLater__)) + " could not be linked.")
  323. def sanitize(self):
  324. remove = []
  325. logger.debug("Sanitizing nodes and references...")
  326. for n in self.nodes:
  327. if n.sanitize() == False:
  328. remove.append(n)
  329. if not len(remove) == 0:
  330. logger.warn(str(len(remove)) + " nodes will be removed because they failed sanitation.")
  331. # FIXME: Some variable ns=0 nodes fail because they don't have DataType fields...
  332. # How should this be handles!?
  333. logger.warn("Not actually removing nodes... it's unclear if this is valid or not")
  334. def getRoot(self):
  335. """ Returns the first node instance with the browseName "Root".
  336. """
  337. return self.getNodeByBrowseName("Root")
  338. def buildEncodingRules(self):
  339. """ Calls buildEncoding() for all DataType nodes (opcua_node_dataType_t).
  340. No return value
  341. """
  342. stat = {True: 0, False: 0}
  343. for n in self.nodes:
  344. if isinstance(n, opcua_node_dataType_t):
  345. n.buildEncoding()
  346. stat[n.isEncodable()] = stat[n.isEncodable()] + 1
  347. logger.debug("Type definitions built/passed: " + str(stat))
  348. def allocateVariables(self):
  349. for n in self.nodes:
  350. if isinstance(n, opcua_node_variable_t):
  351. n.allocateValue()
  352. def getSubTypesOf(self, tdNodes = None, currentNode = None, hasSubtypeRefNode = None):
  353. # If this is a toplevel call, collect the following information as defaults
  354. if tdNodes == None:
  355. tdNodes = []
  356. if currentNode == None:
  357. currentNode = self.getNodeByBrowseName("HasTypeDefinition")
  358. tdNodes.append(currentNode)
  359. if len(tdNodes) < 1:
  360. return []
  361. if hasSubtypeRefNode == None:
  362. hasSubtypeRefNode = self.getNodeByBrowseName("HasSubtype")
  363. if hasSubtypeRefNode == None:
  364. return tdNodes
  365. # collect all subtypes of this node
  366. for ref in currentNode.getReferences():
  367. if ref.isForward() and ref.referenceType().id() == hasSubtypeRefNode.id():
  368. tdNodes.append(ref.target())
  369. self.getTypeDefinitionNodes(tdNodes=tdNodes, currentNode = ref.target(), hasSubtypeRefNode=hasSubtypeRefNode)
  370. return tdNodes
  371. def printDotGraphWalk(self, depth=1, filename="out.dot", rootNode=None,
  372. followInverse = False, excludeNodeIds=[]):
  373. """ Outputs a graphiz/dot description the nodes centered around rootNode.
  374. References beginning from rootNode will be followed for depth steps. If
  375. "followInverse = True" is passed, then inverse (not Forward) references
  376. will also be followed.
  377. Nodes can be excluded from the graph by passing a list of NodeIds as
  378. string representation using excludeNodeIds (ex ["i=53", "ns=2;i=453"]).
  379. Output is written into filename to be parsed by dot/neato/srfp...
  380. """
  381. iter = depth
  382. processed = []
  383. if rootNode == None or \
  384. not isinstance(rootNode, opcua_node_t) or \
  385. not rootNode in self.nodes:
  386. root = self.getRoot()
  387. else:
  388. root = rootNode
  389. file=open(filename, 'w+')
  390. if root == None:
  391. return
  392. file.write("digraph ns {\n")
  393. file.write(root.printDot())
  394. refs=[]
  395. if followInverse == True:
  396. refs = root.getReferences(); # + root.getInverseReferences()
  397. else:
  398. for ref in root.getReferences():
  399. if ref.isForward():
  400. refs.append(ref)
  401. while iter > 0:
  402. tmp = []
  403. for ref in refs:
  404. if isinstance(ref.target(), opcua_node_t):
  405. tgt = ref.target()
  406. if not str(tgt.id()) in excludeNodeIds:
  407. if not tgt in processed:
  408. file.write(tgt.printDot())
  409. processed.append(tgt)
  410. if ref.isForward() == False and followInverse == True:
  411. tmp = tmp + tgt.getReferences(); # + tgt.getInverseReferences()
  412. elif ref.isForward() == True :
  413. tmp = tmp + tgt.getReferences();
  414. refs = tmp
  415. iter = iter - 1
  416. file.write("}\n")
  417. file.close()
  418. def getSubTypesOf2(self, node):
  419. re = [node]
  420. for ref in node.getReferences():
  421. if isinstance(ref.target(), opcua_node_t):
  422. if ref.referenceType().displayName() == "HasSubtype" and ref.isForward():
  423. re = re + self.getSubTypesOf2(ref.target())
  424. return re
  425. def reorderNodesMinDependencies(self, printedExternally):
  426. #Kahn's algorithm
  427. #https://algocoding.wordpress.com/2015/04/05/topological-sorting-python/
  428. relevant_types = ["HierarchicalReferences", "HasComponent"]
  429. temp = []
  430. for t in relevant_types:
  431. temp = temp + self.getSubTypesOf2(self.getNodeByBrowseName(t))
  432. relevant_types = temp
  433. in_degree = { u : 0 for u in self.nodes } # determine in-degree
  434. for u in self.nodes: # of each node
  435. if u not in printedExternally:
  436. for ref in u.getReferences():
  437. if isinstance(ref.target(), opcua_node_t):
  438. if(ref.referenceType() in relevant_types and ref.isForward()):
  439. in_degree[ref.target()] += 1
  440. Q = deque() # collect nodes with zero in-degree
  441. for u in in_degree:
  442. if in_degree[u] == 0:
  443. Q.appendleft(u)
  444. L = [] # list for order of nodes
  445. while Q:
  446. u = Q.pop() # choose node of zero in-degree
  447. L.append(u) # and 'remove' it from graph
  448. for ref in u.getReferences():
  449. if isinstance(ref.target(), opcua_node_t):
  450. if(ref.referenceType() in relevant_types and ref.isForward()):
  451. in_degree[ref.target()] -= 1
  452. if in_degree[ref.target()] == 0:
  453. Q.appendleft(ref.target())
  454. if len(L) == len(self.nodes):
  455. self.nodes = L
  456. else: # if there is a cycle,
  457. logger.error("Node graph is circular on the specified references")
  458. self.nodes = L + [x for x in self.nodes if x not in L]
  459. return