You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

764 lines
25 KiB

  1. #
  2. # KiCad python module for interpreting generic netlists which can be used
  3. # to generate Bills of materials, etc.
  4. #
  5. # Remember these files use UTF8 encoding
  6. #
  7. # No string formatting is used on purpose as the only string formatting that
  8. # is current compatible with python 2.4+ to 3.0+ is the '%' method, and that
  9. # is due to be deprecated in 3.0+ soon
  10. #
  11. """
  12. @package
  13. Helper module for interpreting generic netlist and build custom
  14. bom generators or netlists in foreign format
  15. """
  16. from __future__ import print_function
  17. import sys
  18. import xml.sax as sax
  19. import re
  20. import pdb
  21. #-----<Configure>----------------------------------------------------------------
  22. # excluded_fields is a list of regular expressions. If any one matches a field
  23. # from either a component or a libpart, then that will not be included as a
  24. # column in the BOM. Otherwise all columns from all used libparts and components
  25. # will be unionized and will appear. Some fields are impossible to blacklist, such
  26. # as Ref, Value, Footprint, and Datasheet. Additionally Qty and Item are supplied
  27. # unconditionally as columns, and may not be removed.
  28. excluded_fields = [
  29. #'Price@1000'
  30. ]
  31. # You may exlude components from the BOM by either:
  32. #
  33. # 1) adding a custom field named "Installed" to your components and filling it
  34. # with a value of "NU" (Normally Uninstalled).
  35. # See netlist.getInterestingComponents(), or
  36. #
  37. # 2) blacklisting it in any of the three following lists:
  38. # regular expressions which match component 'Reference' fields of components that
  39. # are to be excluded from the BOM.
  40. excluded_references = [
  41. 'TP[0-9]+' # all test points
  42. ]
  43. # regular expressions which match component 'Value' fields of components that
  44. # are to be excluded from the BOM.
  45. excluded_values = [
  46. 'MOUNTHOLE',
  47. 'SCOPETEST',
  48. 'MOUNT_HOLE',
  49. 'SOLDER_BRIDGE.*'
  50. ]
  51. # regular expressions which match component 'Footprint' fields of components that
  52. # are to be excluded from the BOM.
  53. excluded_footprints = [
  54. #'MOUNTHOLE'
  55. ]
  56. #-----</Configure>---------------------------------------------------------------
  57. class xmlElement():
  58. """xml element which can represent all nodes of the netlist tree. It can be
  59. used to easily generate various output formats by propogating format
  60. requests to children recursively.
  61. """
  62. def __init__(self, name, parent=None):
  63. self.name = name
  64. self.attributes = {}
  65. self.parent = parent
  66. self.chars = ""
  67. self.children = []
  68. def __str__(self):
  69. """String representation of this netlist element
  70. """
  71. return self.name + "[" + self.chars + "]" + " attr_count:" + str(len(self.attributes))
  72. def formatXML(self, nestLevel=0, amChild=False):
  73. """Return this element formatted as XML
  74. Keywords:
  75. nestLevel -- increases by one for each level of nesting.
  76. amChild -- If set to True, the start of document is not returned.
  77. """
  78. s = ""
  79. indent = ""
  80. for i in range(nestLevel):
  81. indent += " "
  82. if not amChild:
  83. s = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
  84. s += indent + "<" + self.name
  85. for a in self.attributes:
  86. s += " " + a + "=\"" + self.attributes[a] + "\""
  87. if (len(self.chars) == 0) and (len(self.children) == 0):
  88. s += "/>"
  89. else:
  90. s += ">" + self.chars
  91. for c in self.children:
  92. s += "\n"
  93. s += c.formatXML(nestLevel+1, True)
  94. if (len(self.children) > 0):
  95. s += "\n" + indent
  96. if (len(self.children) > 0) or (len(self.chars) > 0):
  97. s += "</" + self.name + ">"
  98. return s
  99. def formatHTML(self, amChild=False):
  100. """Return this element formatted as HTML
  101. Keywords:
  102. amChild -- If set to True, the start of document is not returned
  103. """
  104. s = ""
  105. if not amChild:
  106. s = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  107. "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  108. <html xmlns="http://www.w3.org/1999/xhtml">
  109. <head>
  110. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  111. <title></title>
  112. </head>
  113. <body>
  114. <table>
  115. """
  116. s += "<tr><td><b>" + self.name + "</b><br>" + self.chars + "</td><td><ul>"
  117. for a in self.attributes:
  118. s += "<li>" + a + " = " + self.attributes[a] + "</li>"
  119. s += "</ul></td></tr>\n"
  120. for c in self.children:
  121. s += c.formatHTML(True)
  122. if not amChild:
  123. s += """</table>
  124. </body>
  125. </html>"""
  126. return s
  127. def addAttribute(self, attr, value):
  128. """Add an attribute to this element"""
  129. if type(value) != str: value = value.encode('utf-8')
  130. self.attributes[attr] = value
  131. def setAttribute(self, attr, value):
  132. """Set an attributes value - in fact does the same thing as add
  133. attribute
  134. """
  135. self.attributes[attr] = value
  136. def setChars(self, chars):
  137. """Set the characters for this element"""
  138. self.chars = chars
  139. def addChars(self, chars):
  140. """Add characters (textual value) to this element"""
  141. self.chars += chars
  142. def addChild(self, child):
  143. """Add a child element to this element"""
  144. self.children.append(child)
  145. return self.children[len(self.children) - 1]
  146. def getParent(self):
  147. """Get the parent of this element (Could be None)"""
  148. return self.parent
  149. def getChild(self, name):
  150. """Returns the first child element named 'name'
  151. Keywords:
  152. name -- The name of the child element to return"""
  153. for child in self.children:
  154. if child.name == name:
  155. return child
  156. return None
  157. def getChildren(self, name=None):
  158. if name:
  159. # return _all_ children named "name"
  160. ret = []
  161. for child in self.children:
  162. if child.name == name:
  163. ret.append(child)
  164. return ret
  165. else:
  166. return self.children
  167. def get(self, elemName, attribute="", attrmatch=""):
  168. """Return the text data for either an attribute or an xmlElement
  169. """
  170. if (self.name == elemName):
  171. if attribute != "":
  172. try:
  173. if attrmatch != "":
  174. if self.attributes[attribute] == attrmatch:
  175. ret = self.chars
  176. if type(ret) != str: ret = ret.encode('utf-8')
  177. return ret
  178. else:
  179. ret = self.attributes[attribute]
  180. if type(ret) != str: ret = ret.encode('utf-8')
  181. return ret
  182. except AttributeError:
  183. ret = ""
  184. if type(ret) != str: ret = ret.encode('utf-8')
  185. return ret
  186. else:
  187. ret = self.chars
  188. if type(ret) != str: ret = ret.encode('utf-8')
  189. return ret
  190. for child in self.children:
  191. ret = child.get(elemName, attribute, attrmatch)
  192. if ret != "":
  193. if type(ret) != str: ret = ret.encode('utf-8')
  194. return ret
  195. ret = ""
  196. if type(ret) != str: ret = ret.encode('utf-8')
  197. return ret
  198. class libpart():
  199. """Class for a library part, aka 'libpart' in the xml netlist file.
  200. (Components in eeschema are instantiated from library parts.)
  201. This part class is implemented by wrapping an xmlElement with accessors.
  202. This xmlElement instance is held in field 'element'.
  203. """
  204. def __init__(self, xml_element):
  205. #
  206. self.element = xml_element
  207. #def __str__(self):
  208. # simply print the xmlElement associated with this part
  209. #return str(self.element)
  210. def getLibName(self):
  211. return self.element.get("libpart", "lib")
  212. def getPartName(self):
  213. return self.element.get("libpart", "part")
  214. def getDescription(self):
  215. return self.element.get("description")
  216. def getField(self, name):
  217. return self.element.get("field", "name", name)
  218. def getFieldNames(self):
  219. """Return a list of field names in play for this libpart.
  220. """
  221. fieldNames = []
  222. fields = self.element.getChild('fields')
  223. if fields:
  224. for f in fields.getChildren():
  225. fieldNames.append( f.get('field','name') )
  226. return fieldNames
  227. def getDatasheet(self):
  228. return self.getField("Datasheet")
  229. def getFootprint(self):
  230. return self.getField("Footprint")
  231. def getAliases(self):
  232. """Return a list of aliases or None"""
  233. aliases = self.element.getChild("aliases")
  234. if aliases:
  235. ret = []
  236. children = aliases.getChildren()
  237. # grab the text out of each child:
  238. for child in children:
  239. ret.append( child.get("alias") )
  240. return ret
  241. return None
  242. class comp():
  243. """Class for a component, aka 'comp' in the xml netlist file.
  244. This component class is implemented by wrapping an xmlElement instance
  245. with accessors. The xmlElement is held in field 'element'.
  246. """
  247. def __init__(self, xml_element):
  248. self.element = xml_element
  249. self.libpart = None
  250. # Set to true when this component is included in a component group
  251. self.grouped = False
  252. def __eq__(self, other):
  253. """ Equivalency operator, remember this can be easily overloaded
  254. 2 components are equivalent ( i.e. can be grouped
  255. if they have same value and same footprint
  256. Override the component equivalence operator must be done before
  257. loading the netlist, otherwise all components will have the original
  258. equivalency operator.
  259. You have to define a comparison module (for instance named myEqu)
  260. and add the line;
  261. kicad_netlist_reader.comp.__eq__ = myEqu
  262. in your bom generator script before calling the netliste reader by something like:
  263. net = kicad_netlist_reader.netlist(sys.argv[1])
  264. """
  265. result = False
  266. if self.getValue() == other.getValue():
  267. if self.getFootprint() == other.getFootprint():
  268. result = True
  269. return result
  270. def setLibPart(self, part):
  271. self.libpart = part
  272. def getLibPart(self):
  273. return self.libpart
  274. def getPartName(self):
  275. return self.element.get("libsource", "part")
  276. def getLibName(self):
  277. return self.element.get("libsource", "lib")
  278. def setValue(self, value):
  279. """Set the value of this component"""
  280. v = self.element.getChild("value")
  281. if v:
  282. v.setChars(value)
  283. def getValue(self):
  284. return self.element.get("value")
  285. def getField(self, name, libraryToo=True):
  286. """Return the value of a field named name. The component is first
  287. checked for the field, and then the components library part is checked
  288. for the field. If the field doesn't exist in either, an empty string is
  289. returned
  290. Keywords:
  291. name -- The name of the field to return the value for
  292. libraryToo -- look in the libpart's fields for the same name if not found
  293. in component itself
  294. """
  295. field = self.element.get("field", "name", name)
  296. if field == "" and libraryToo:
  297. field = self.libpart.getField(name)
  298. return field
  299. def getFieldNames(self):
  300. """Return a list of field names in play for this component. Mandatory
  301. fields are not included, and they are: Value, Footprint, Datasheet, Ref.
  302. The netlist format only includes fields with non-empty values. So if a field
  303. is empty, it will not be present in the returned list.
  304. """
  305. fieldNames = []
  306. fields = self.element.getChild('fields')
  307. if fields:
  308. for f in fields.getChildren():
  309. fieldNames.append( f.get('field','name') )
  310. return fieldNames
  311. def getRef(self):
  312. return self.element.get("comp", "ref")
  313. def getFootprint(self, libraryToo=True):
  314. ret = self.element.get("footprint")
  315. if ret =="" and libraryToo:
  316. ret = self.libpart.getFootprint()
  317. return ret
  318. def getDatasheet(self, libraryToo=True):
  319. ret = self.element.get("datasheet")
  320. if ret == '' and libraryToo:
  321. ret = self.libpart.getDatasheet()
  322. return ret
  323. def getTimestamp(self):
  324. return self.element.get("tstamp")
  325. def getDescription(self):
  326. return self.libpart.getDescription()
  327. class netlist():
  328. """ Kicad generic netlist class. Generally loaded from a kicad generic
  329. netlist file. Includes several helper functions to ease BOM creating
  330. scripts
  331. """
  332. def __init__(self, fname=""):
  333. """Initialiser for the genericNetlist class
  334. Keywords:
  335. fname -- The name of the generic netlist file to open (Optional)
  336. """
  337. self.design = None
  338. self.components = []
  339. self.libparts = []
  340. self.libraries = []
  341. self.nets = []
  342. # The entire tree is loaded into self.tree
  343. self.tree = []
  344. self._curr_element = None
  345. # component blacklist regexs, made from exluded_* above.
  346. self.excluded_references = []
  347. self.excluded_values = []
  348. self.excluded_footprints = []
  349. if fname != "":
  350. self.load(fname)
  351. def addChars(self, content):
  352. """Add characters to the current element"""
  353. self._curr_element.addChars(content)
  354. def addElement(self, name):
  355. """Add a new kicad generic element to the list"""
  356. if self._curr_element == None:
  357. self.tree = xmlElement(name)
  358. self._curr_element = self.tree
  359. else:
  360. self._curr_element = self._curr_element.addChild(
  361. xmlElement(name, self._curr_element))
  362. # If this element is a component, add it to the components list
  363. if self._curr_element.name == "comp":
  364. self.components.append(comp(self._curr_element))
  365. # Assign the design element
  366. if self._curr_element.name == "design":
  367. self.design = self._curr_element
  368. # If this element is a library part, add it to the parts list
  369. if self._curr_element.name == "libpart":
  370. self.libparts.append(libpart(self._curr_element))
  371. # If this element is a net, add it to the nets list
  372. if self._curr_element.name == "net":
  373. self.nets.append(self._curr_element)
  374. # If this element is a library, add it to the libraries list
  375. if self._curr_element.name == "library":
  376. self.libraries.append(self._curr_element)
  377. return self._curr_element
  378. def endDocument(self):
  379. """Called when the netlist document has been fully parsed"""
  380. # When the document is complete, the library parts must be linked to
  381. # the components as they are seperate in the tree so as not to
  382. # duplicate library part information for every component
  383. for c in self.components:
  384. for p in self.libparts:
  385. if p.getLibName() == c.getLibName():
  386. if p.getPartName() == c.getPartName():
  387. c.setLibPart(p)
  388. break
  389. else:
  390. aliases = p.getAliases()
  391. if aliases and self.aliasMatch( c.getPartName(), aliases ):
  392. c.setLibPart(p)
  393. break;
  394. if not c.getLibPart():
  395. print( 'missing libpart for ref:', c.getRef(), c.getPartName(), c.getLibName() )
  396. def aliasMatch(self, partName, aliasList):
  397. for alias in aliasList:
  398. if partName == alias:
  399. return True
  400. return False
  401. def endElement(self):
  402. """End the current element and switch to its parent"""
  403. self._curr_element = self._curr_element.getParent()
  404. def getDate(self):
  405. """Return the date + time string generated by the tree creation tool"""
  406. return self.design.get("date")
  407. def getSource(self):
  408. """Return the source string for the design"""
  409. return self.design.get("source")
  410. def getTool(self):
  411. """Return the tool string which was used to create the netlist tree"""
  412. return self.design.get("tool")
  413. def gatherComponentFieldUnion(self, components=None):
  414. """Gather the complete 'set' of unique component fields, fields found in any component.
  415. """
  416. if not components:
  417. components=self.components
  418. s = set()
  419. for c in components:
  420. s.update( c.getFieldNames() )
  421. # omit anything matching any regex in excluded_fields
  422. ret = set()
  423. for field in s:
  424. exclude = False
  425. for rex in excluded_fields:
  426. if re.match( rex, field ):
  427. exclude = True
  428. break
  429. if not exclude:
  430. ret.add(field)
  431. return ret # this is a python 'set'
  432. def gatherLibPartFieldUnion(self):
  433. """Gather the complete 'set' of part fields, fields found in any part.
  434. """
  435. s = set()
  436. for p in self.libparts:
  437. s.update( p.getFieldNames() )
  438. # omit anything matching any regex in excluded_fields
  439. ret = set()
  440. for field in s:
  441. exclude = False
  442. for rex in excluded_fields:
  443. if re.match( rex, field ):
  444. exclude = True
  445. break
  446. if not exclude:
  447. ret.add(field)
  448. return ret # this is a python 'set'
  449. def getInterestingComponents(self):
  450. """Return a subset of all components, those that should show up in the BOM.
  451. Omit those that should not, by consulting the blacklists:
  452. excluded_values, excluded_refs, and excluded_footprints, which hold one
  453. or more regular expressions. If any of the the regular expressions match
  454. the corresponding field's value in a component, then the component is exluded.
  455. """
  456. # pre-compile all the regex expressions:
  457. del self.excluded_references[:]
  458. del self.excluded_values[:]
  459. del self.excluded_footprints[:]
  460. for rex in excluded_references:
  461. self.excluded_references.append( re.compile( rex ) )
  462. for rex in excluded_values:
  463. self.excluded_values.append( re.compile( rex ) )
  464. for rex in excluded_footprints:
  465. self.excluded_footprints.append( re.compile( rex ) )
  466. # the subset of components to return, considered as "interesting".
  467. ret = []
  468. # run each component thru a series of tests, if it passes all, then add it
  469. # to the interesting list 'ret'.
  470. for c in self.components:
  471. exclude = False
  472. if not exclude:
  473. for refs in self.excluded_references:
  474. if refs.match(c.getRef()):
  475. exclude = True
  476. break;
  477. if not exclude:
  478. for vals in self.excluded_values:
  479. if vals.match(c.getValue()):
  480. exclude = True
  481. break;
  482. if not exclude:
  483. for mods in self.excluded_footprints:
  484. if mods.match(c.getFootprint()):
  485. exclude = True
  486. break;
  487. if not exclude:
  488. # This is a fairly personal way to flag DNS (Do Not Stuff). NU for
  489. # me means Normally Uninstalled. You can 'or in' another expression here.
  490. if c.getField( "Installed" ) == 'NU':
  491. exclude = True
  492. if not exclude:
  493. ret.append(c)
  494. # Sort first by ref as this makes for easier to read BOM's
  495. def f(v):
  496. return re.sub(r'([A-z]+)[0-9]+', r'\1', v) + '%08i' % int(re.sub(r'[A-z]+([0-9]+)', r'\1', v))
  497. ret.sort(key=lambda g: f(g.getRef()))
  498. return ret
  499. def groupComponents(self, components = None):
  500. """Return a list of component lists. Components are grouped together
  501. when the value, library and part identifiers match.
  502. Keywords:
  503. components -- is a list of components, typically an interesting subset
  504. of all components, or None. If None, then all components are looked at.
  505. """
  506. if not components:
  507. components = self.components
  508. groups = []
  509. # Make sure to start off will all components ungrouped to begin with
  510. for c in components:
  511. c.grouped = False
  512. # Group components based on the value, library and part identifiers
  513. for c in components:
  514. if c.grouped == False:
  515. c.grouped = True
  516. newgroup = []
  517. newgroup.append(c)
  518. # Check every other ungrouped component against this component
  519. # and add to the group as necessary
  520. for ci in components:
  521. if ci.grouped == False and ci == c:
  522. newgroup.append(ci)
  523. ci.grouped = True
  524. # Add the new component group to the groups list
  525. groups.append(newgroup)
  526. # Each group is a list of components, we need to sort each list first
  527. # to get them in order as this makes for easier to read BOM's
  528. def f(v):
  529. return re.sub(r'([A-z]+)[0-9]+', r'\1', v) + '%08i' % int(re.sub(r'[A-z]+([0-9]+)', r'\1', v))
  530. for g in groups:
  531. g = sorted(g, key=lambda g: f(g.getRef()))
  532. # Finally, sort the groups to order the references alphabetically
  533. groups = sorted(groups, key=lambda group: f(group[0].getRef()))
  534. return groups
  535. def getGroupField(self, group, field):
  536. """Return the whatever is known about the given field by consulting each
  537. component in the group. If any of them know something about the property/field,
  538. then return that first non-blank value.
  539. """
  540. for c in group:
  541. ret = c.getField(field, False)
  542. if ret != '':
  543. return ret
  544. return group[0].getLibPart().getField(field)
  545. def getGroupFootprint(self, group):
  546. """Return the whatever is known about the Footprint by consulting each
  547. component in the group. If any of them know something about the Footprint,
  548. then return that first non-blank value.
  549. """
  550. for c in group:
  551. ret = c.getFootprint()
  552. if ret != "":
  553. return ret
  554. return group[0].getLibPart().getFootprint()
  555. def getGroupDatasheet(self, group):
  556. """Return the whatever is known about the Datasheet by consulting each
  557. component in the group. If any of them know something about the Datasheet,
  558. then return that first non-blank value.
  559. """
  560. for c in group:
  561. ret = c.getDatasheet()
  562. if ret != "":
  563. return ret
  564. if len(group) > 0:
  565. return group[0].getLibPart().getDatasheet()
  566. else:
  567. print("NULL!")
  568. return ''
  569. def formatXML(self):
  570. """Return the whole netlist formatted in XML"""
  571. return self.tree.formatXML()
  572. def formatHTML(self):
  573. """Return the whole netlist formatted in HTML"""
  574. return self.tree.formatHTML()
  575. def load(self, fname):
  576. """Load a kicad generic netlist
  577. Keywords:
  578. fname -- The name of the generic netlist file to open
  579. """
  580. try:
  581. self._reader = sax.make_parser()
  582. self._reader.setContentHandler(_gNetReader(self))
  583. self._reader.parse(fname)
  584. except IOError as e:
  585. print( __file__, ":", e, file=sys.stderr )
  586. sys.exit(-1)
  587. class _gNetReader(sax.handler.ContentHandler):
  588. """SAX kicad generic netlist content handler - passes most of the work back
  589. to the 'netlist' class which builds a complete tree in RAM for the design
  590. """
  591. def __init__(self, aParent):
  592. self.parent = aParent
  593. def startElement(self, name, attrs):
  594. """Start of a new XML element event"""
  595. element = self.parent.addElement(name)
  596. for name in attrs.getNames():
  597. element.addAttribute(name, attrs.getValue(name))
  598. def endElement(self, name):
  599. self.parent.endElement()
  600. def characters(self, content):
  601. # Ignore erroneous white space - ignoreableWhitespace does not get rid
  602. # of the need for this!
  603. if not content.isspace():
  604. self.parent.addChars(content)
  605. def endDocument(self):
  606. """End of the XML document event"""
  607. self.parent.endDocument()