kicad_netlist_reader.py 25 KB

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