123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- #
- # KiCad python module for interpreting generic netlists which can be used
- # to generate Bills of materials, etc.
- #
- # Remember these files use UTF8 encoding
- #
- # No string formatting is used on purpose as the only string formatting that
- # is current compatible with python 2.4+ to 3.0+ is the '%' method, and that
- # is due to be deprecated in 3.0+ soon
- #
- """
- @package
- Helper module for interpreting generic netlist and build custom
- bom generators or netlists in foreign format
- """
- from __future__ import print_function
- import sys
- import xml.sax as sax
- import re
- import pdb
- import string
- #-----<Configure>----------------------------------------------------------------
- # excluded_fields is a list of regular expressions. If any one matches a field
- # from either a component or a libpart, then that will not be included as a
- # column in the BOM. Otherwise all columns from all used libparts and components
- # will be unionized and will appear. Some fields are impossible to blacklist, such
- # as Ref, Value, Footprint, and Datasheet. Additionally Qty and Item are supplied
- # unconditionally as columns, and may not be removed.
- excluded_fields = [
- #'Price@1000'
- ]
- # You may exlude components from the BOM by either:
- #
- # 1) adding a custom field named "Installed" to your components and filling it
- # with a value of "NU" (Normally Uninstalled).
- # See netlist.getInterestingComponents(), or
- #
- # 2) blacklisting it in any of the three following lists:
- # regular expressions which match component 'Reference' fields of components that
- # are to be excluded from the BOM.
- excluded_references = [
- 'TP[0-9]+' # all test points
- ]
- # regular expressions which match component 'Value' fields of components that
- # are to be excluded from the BOM.
- excluded_values = [
- 'MOUNTHOLE',
- 'SCOPETEST',
- 'MOUNT_HOLE',
- 'SOLDER_BRIDGE.*'
- ]
- # regular expressions which match component 'Footprint' fields of components that
- # are to be excluded from the BOM.
- excluded_footprints = [
- #'MOUNTHOLE'
- ]
- #-----</Configure>---------------------------------------------------------------
- class xmlElement():
- """xml element which can represent all nodes of the netlist tree. It can be
- used to easily generate various output formats by propogating format
- requests to children recursively.
- """
- def __init__(self, name, parent=None):
- self.name = name
- self.attributes = {}
- self.parent = parent
- self.chars = ""
- self.children = []
- def __str__(self):
- """String representation of this netlist element
- """
- return self.name + "[" + self.chars + "]" + " attr_count:" + str(len(self.attributes))
- def formatXML(self, nestLevel=0, amChild=False):
- """Return this element formatted as XML
- Keywords:
- nestLevel -- increases by one for each level of nesting.
- amChild -- If set to True, the start of document is not returned.
- """
- s = ""
- indent = ""
- for i in range(nestLevel):
- indent += " "
- if not amChild:
- s = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
- s += indent + "<" + self.name
- for a in self.attributes:
- s += " " + a + "=\"" + self.attributes[a] + "\""
- if (len(self.chars) == 0) and (len(self.children) == 0):
- s += "/>"
- else:
- s += ">" + self.chars
- for c in self.children:
- s += "\n"
- s += c.formatXML(nestLevel+1, True)
- if (len(self.children) > 0):
- s += "\n" + indent
- if (len(self.children) > 0) or (len(self.chars) > 0):
- s += "</" + self.name + ">"
- return s
- def formatHTML(self, amChild=False):
- """Return this element formatted as HTML
- Keywords:
- amChild -- If set to True, the start of document is not returned
- """
- s = ""
- if not amChild:
- s = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <title></title>
- </head>
- <body>
- <table>
- """
- s += "<tr><td><b>" + self.name + "</b><br>" + self.chars + "</td><td><ul>"
- for a in self.attributes:
- s += "<li>" + a + " = " + self.attributes[a] + "</li>"
- s += "</ul></td></tr>\n"
- for c in self.children:
- s += c.formatHTML(True)
- if not amChild:
- s += """</table>
- </body>
- </html>"""
- return s
- def addAttribute(self, attr, value):
- """Add an attribute to this element"""
- if type(value) != str: value = value.encode('utf-8')
- self.attributes[attr] = value
- def setAttribute(self, attr, value):
- """Set an attributes value - in fact does the same thing as add
- attribute
- """
- self.attributes[attr] = value
- def setChars(self, chars):
- """Set the characters for this element"""
- self.chars = chars
- def addChars(self, chars):
- """Add characters (textual value) to this element"""
- self.chars += chars
- def addChild(self, child):
- """Add a child element to this element"""
- self.children.append(child)
- return self.children[len(self.children) - 1]
- def getParent(self):
- """Get the parent of this element (Could be None)"""
- return self.parent
- def getChild(self, name):
- """Returns the first child element named 'name'
- Keywords:
- name -- The name of the child element to return"""
- for child in self.children:
- if child.name == name:
- return child
- return None
- def getChildren(self, name=None):
- if name:
- # return _all_ children named "name"
- ret = []
- for child in self.children:
- if child.name == name:
- ret.append(child)
- return ret
- else:
- return self.children
- def get(self, elemName, attribute="", attrmatch=""):
- """Return the text data for either an attribute or an xmlElement
- """
- if (self.name == elemName):
- if attribute != "":
- try:
- if attrmatch != "":
- if self.attributes[attribute] == attrmatch:
- ret = self.chars
- if type(ret) != str: ret = ret.encode('utf-8')
- return ret
- else:
- ret = self.attributes[attribute]
- if type(ret) != str: ret = ret.encode('utf-8')
- return ret
- except AttributeError:
- ret = ""
- if type(ret) != str: ret = ret.encode('utf-8')
- return ret
- else:
- ret = self.chars
- if type(ret) != str: ret = ret.encode('utf-8')
- return ret
- for child in self.children:
- ret = child.get(elemName, attribute, attrmatch)
- if ret != "":
- if type(ret) != str: ret = ret.encode('utf-8')
- return ret
- ret = ""
- if type(ret) != str: ret = ret.encode('utf-8')
- return ret
- class libpart():
- """Class for a library part, aka 'libpart' in the xml netlist file.
- (Components in eeschema are instantiated from library parts.)
- This part class is implemented by wrapping an xmlElement with accessors.
- This xmlElement instance is held in field 'element'.
- """
- def __init__(self, xml_element):
- #
- self.element = xml_element
- #def __str__(self):
- # simply print the xmlElement associated with this part
- #return str(self.element)
- def getLibName(self):
- return self.element.get("libpart", "lib")
- def getPartName(self):
- return self.element.get("libpart", "part")
- def getDescription(self):
- return self.element.get("description")
- def getField(self, name):
- return self.element.get("field", "name", name)
- def getFieldNames(self):
- """Return a list of field names in play for this libpart.
- """
- fieldNames = []
- fields = self.element.getChild('fields')
- if fields:
- for f in fields.getChildren():
- fieldNames.append( f.get('field','name') )
- return fieldNames
- def getDatasheet(self):
- return self.getField("Datasheet")
- def getFootprint(self):
- return self.getField("Footprint")
- def getAliases(self):
- """Return a list of aliases or None"""
- aliases = self.element.getChild("aliases")
- if aliases:
- ret = []
- children = aliases.getChildren()
- # grab the text out of each child:
- for child in children:
- ret.append( child.get("alias") )
- return ret
- return None
- class comp():
- """Class for a component, aka 'comp' in the xml netlist file.
- This component class is implemented by wrapping an xmlElement instance
- with accessors. The xmlElement is held in field 'element'.
- """
- def __init__(self, xml_element):
- self.element = xml_element
- self.libpart = None
- # Set to true when this component is included in a component group
- self.grouped = False
- def __eq__(self, other):
- """ Equivalency operator, remember this can be easily overloaded
- 2 components are equivalent ( i.e. can be grouped
- if they have same value and same footprint
- Override the component equivalence operator must be done before
- loading the netlist, otherwise all components will have the original
- equivalency operator.
- You have to define a comparison module (for instance named myEqu)
- and add the line;
- kicad_netlist_reader.comp.__eq__ = myEqu
- in your bom generator script before calling the netliste reader by something like:
- net = kicad_netlist_reader.netlist(sys.argv[1])
- """
- result = False
- if self.getValue() == other.getValue():
- if self.getFootprint() == other.getFootprint():
- if self.getRef().rstrip(string.digits) == other.getRef().rstrip(string.digits):
- result = True
- return result
- def setLibPart(self, part):
- self.libpart = part
- def getLibPart(self):
- return self.libpart
- def getPartName(self):
- return self.element.get("libsource", "part")
- def getLibName(self):
- return self.element.get("libsource", "lib")
- def setValue(self, value):
- """Set the value of this component"""
- v = self.element.getChild("value")
- if v:
- v.setChars(value)
- def getValue(self):
- return self.element.get("value")
- def getField(self, name, libraryToo=True):
- """Return the value of a field named name. The component is first
- checked for the field, and then the components library part is checked
- for the field. If the field doesn't exist in either, an empty string is
- returned
- Keywords:
- name -- The name of the field to return the value for
- libraryToo -- look in the libpart's fields for the same name if not found
- in component itself
- """
- field = self.element.get("field", "name", name)
- if field == "" and libraryToo and self.libpart:
- field = self.libpart.getField(name)
- return field
- def getFieldNames(self):
- """Return a list of field names in play for this component. Mandatory
- fields are not included, and they are: Value, Footprint, Datasheet, Ref.
- The netlist format only includes fields with non-empty values. So if a field
- is empty, it will not be present in the returned list.
- """
- fieldNames = []
- fields = self.element.getChild('fields')
- if fields:
- for f in fields.getChildren():
- fieldNames.append( f.get('field','name') )
- return fieldNames
- def getRef(self):
- return self.element.get("comp", "ref")
- def getFootprint(self, libraryToo=True):
- ret = self.element.get("footprint")
- if ret == "" and libraryToo and self.libpart:
- ret = self.libpart.getFootprint()
- return ret
- def getDatasheet(self, libraryToo=True):
- ret = self.element.get("datasheet")
- if ret == "" and libraryToo and self.libpart:
- ret = self.libpart.getDatasheet()
- return ret
- def getTimestamp(self):
- return self.element.get("tstamp")
- def getDescription(self):
- return self.element.get("libsource", "description")
- class netlist():
- """ Kicad generic netlist class. Generally loaded from a kicad generic
- netlist file. Includes several helper functions to ease BOM creating
- scripts
- """
- def __init__(self, fname=""):
- """Initialiser for the genericNetlist class
- Keywords:
- fname -- The name of the generic netlist file to open (Optional)
- """
- self.design = None
- self.components = []
- self.libparts = []
- self.libraries = []
- self.nets = []
- # The entire tree is loaded into self.tree
- self.tree = []
- self._curr_element = None
- # component blacklist regexs, made from exluded_* above.
- self.excluded_references = []
- self.excluded_values = []
- self.excluded_footprints = []
- if fname != "":
- self.load(fname)
- def addChars(self, content):
- """Add characters to the current element"""
- self._curr_element.addChars(content)
- def addElement(self, name):
- """Add a new kicad generic element to the list"""
- if self._curr_element == None:
- self.tree = xmlElement(name)
- self._curr_element = self.tree
- else:
- self._curr_element = self._curr_element.addChild(
- xmlElement(name, self._curr_element))
- # If this element is a component, add it to the components list
- if self._curr_element.name == "comp":
- self.components.append(comp(self._curr_element))
- # Assign the design element
- if self._curr_element.name == "design":
- self.design = self._curr_element
- # If this element is a library part, add it to the parts list
- if self._curr_element.name == "libpart":
- self.libparts.append(libpart(self._curr_element))
- # If this element is a net, add it to the nets list
- if self._curr_element.name == "net":
- self.nets.append(self._curr_element)
- # If this element is a library, add it to the libraries list
- if self._curr_element.name == "library":
- self.libraries.append(self._curr_element)
- return self._curr_element
- def endDocument(self):
- """Called when the netlist document has been fully parsed"""
- # When the document is complete, the library parts must be linked to
- # the components as they are seperate in the tree so as not to
- # duplicate library part information for every component
- for c in self.components:
- for p in self.libparts:
- if p.getLibName() == c.getLibName():
- if p.getPartName() == c.getPartName():
- c.setLibPart(p)
- break
- else:
- aliases = p.getAliases()
- if aliases and self.aliasMatch( c.getPartName(), aliases ):
- c.setLibPart(p)
- break;
- if not c.getLibPart():
- print( 'missing libpart for ref:', c.getRef(), c.getPartName(), c.getLibName() )
- def aliasMatch(self, partName, aliasList):
- for alias in aliasList:
- if partName == alias:
- return True
- return False
- def endElement(self):
- """End the current element and switch to its parent"""
- self._curr_element = self._curr_element.getParent()
- def getDate(self):
- """Return the date + time string generated by the tree creation tool"""
- return self.design.get("date")
- def getSource(self):
- """Return the source string for the design"""
- return self.design.get("source")
- def getTool(self):
- """Return the tool string which was used to create the netlist tree"""
- return self.design.get("tool")
- def gatherComponentFieldUnion(self, components=None):
- """Gather the complete 'set' of unique component fields, fields found in any component.
- """
- if not components:
- components=self.components
- s = set()
- for c in components:
- s.update( c.getFieldNames() )
- # omit anything matching any regex in excluded_fields
- ret = set()
- for field in s:
- exclude = False
- for rex in excluded_fields:
- if re.match( rex, field ):
- exclude = True
- break
- if not exclude:
- ret.add(field)
- return ret # this is a python 'set'
- def gatherLibPartFieldUnion(self):
- """Gather the complete 'set' of part fields, fields found in any part.
- """
- s = set()
- for p in self.libparts:
- s.update( p.getFieldNames() )
- # omit anything matching any regex in excluded_fields
- ret = set()
- for field in s:
- exclude = False
- for rex in excluded_fields:
- if re.match( rex, field ):
- exclude = True
- break
- if not exclude:
- ret.add(field)
- return ret # this is a python 'set'
- def getInterestingComponents(self):
- """Return a subset of all components, those that should show up in the BOM.
- Omit those that should not, by consulting the blacklists:
- excluded_values, excluded_refs, and excluded_footprints, which hold one
- or more regular expressions. If any of the the regular expressions match
- the corresponding field's value in a component, then the component is exluded.
- """
- # pre-compile all the regex expressions:
- del self.excluded_references[:]
- del self.excluded_values[:]
- del self.excluded_footprints[:]
- for rex in excluded_references:
- self.excluded_references.append( re.compile( rex ) )
- for rex in excluded_values:
- self.excluded_values.append( re.compile( rex ) )
- for rex in excluded_footprints:
- self.excluded_footprints.append( re.compile( rex ) )
- # the subset of components to return, considered as "interesting".
- ret = []
- # run each component thru a series of tests, if it passes all, then add it
- # to the interesting list 'ret'.
- for c in self.components:
- exclude = False
- if not exclude:
- for refs in self.excluded_references:
- if refs.match(c.getRef()):
- exclude = True
- break;
- if not exclude:
- for vals in self.excluded_values:
- if vals.match(c.getValue()):
- exclude = True
- break;
- if not exclude:
- for mods in self.excluded_footprints:
- if mods.match(c.getFootprint()):
- exclude = True
- break;
- if not exclude:
- # This is a fairly personal way to flag DNS (Do Not Stuff). NU for
- # me means Normally Uninstalled. You can 'or in' another expression here.
- if c.getField( "Installed" ) == 'NU':
- exclude = True
- if not exclude:
- ret.append(c)
- # The key to sort the components in the BOM
- # This sorts using a natural sorting order (e.g. 100 after 99), and if it wasn't used
- # the normal sort would place 100 before 99 since it only would look at the first digit.
- def sortKey( str ):
- return [ int(t) if t.isdigit() else t.lower()
- for t in re.split( '(\d+)', str ) ]
- ret.sort(key=lambda g: sortKey(g.getRef()))
- return ret
- def groupComponents(self, components = None):
- """Return a list of component lists. Components are grouped together
- when the value, library and part identifiers match.
- Keywords:
- components -- is a list of components, typically an interesting subset
- of all components, or None. If None, then all components are looked at.
- """
- if not components:
- components = self.components
- groups = []
- # Make sure to start off will all components ungrouped to begin with
- for c in components:
- c.grouped = False
- # Group components based on the value, library and part identifiers
- for c in components:
- if c.grouped == False:
- c.grouped = True
- newgroup = []
- newgroup.append(c)
- # Check every other ungrouped component against this component
- # and add to the group as necessary
- for ci in components:
- if ci.grouped == False and ci == c:
- newgroup.append(ci)
- ci.grouped = True
- # Add the new component group to the groups list
- groups.append(newgroup)
- # The key to sort the components in the BOM
- # This sorts using a natural sorting order (e.g. 100 after 99), and if it wasn't used
- # the normal sort would place 100 before 99 since it only would look at the first digit.
- def sortKey( str ):
- return [ int(t) if t.isdigit() else t.lower()
- for t in re.split( '(\d+)', str ) ]
- for g in groups:
- #g = sorted(g, key=lambda g: sortKey(g.getRef()))
- g.sort(key=lambda g: sortKey(g.getRef()))
- # Finally, sort the groups to order the references alphabetically
- groups.sort(key=lambda group: sortKey(group[0].getRef()))
- return groups
- def getGroupField(self, group, field):
- """Return the whatever is known about the given field by consulting each
- component in the group. If any of them know something about the property/field,
- then return that first non-blank value.
- """
- for c in group:
- ret = c.getField(field, False)
- if ret != '':
- return ret
- libpart = group[0].getLibPart()
- if not libpart:
- return ''
- return libpart.getField(field)
- def getGroupFootprint(self, group):
- """Return the whatever is known about the Footprint by consulting each
- component in the group. If any of them know something about the Footprint,
- then return that first non-blank value.
- """
- for c in group:
- ret = c.getFootprint()
- if ret != "":
- return ret
- return group[0].getLibPart().getFootprint()
- def getGroupDatasheet(self, group):
- """Return the whatever is known about the Datasheet by consulting each
- component in the group. If any of them know something about the Datasheet,
- then return that first non-blank value.
- """
- for c in group:
- ret = c.getDatasheet()
- if ret != "":
- return ret
- if len(group) > 0:
- return group[0].getLibPart().getDatasheet()
- else:
- print("NULL!")
- return ''
- def formatXML(self):
- """Return the whole netlist formatted in XML"""
- return self.tree.formatXML()
- def formatHTML(self):
- """Return the whole netlist formatted in HTML"""
- return self.tree.formatHTML()
- def load(self, fname):
- """Load a kicad generic netlist
- Keywords:
- fname -- The name of the generic netlist file to open
- """
- try:
- self._reader = sax.make_parser()
- self._reader.setContentHandler(_gNetReader(self))
- self._reader.parse(fname)
- except IOError as e:
- print( __file__, ":", e, file=sys.stderr )
- sys.exit(-1)
- class _gNetReader(sax.handler.ContentHandler):
- """SAX kicad generic netlist content handler - passes most of the work back
- to the 'netlist' class which builds a complete tree in RAM for the design
- """
- def __init__(self, aParent):
- self.parent = aParent
- def startElement(self, name, attrs):
- """Start of a new XML element event"""
- element = self.parent.addElement(name)
- for name in attrs.getNames():
- element.addAttribute(name, attrs.getValue(name))
- def endElement(self, name):
- self.parent.endElement()
- def characters(self, content):
- # Ignore erroneous white space - ignoreableWhitespace does not get rid
- # of the need for this!
- if not content.isspace():
- self.parent.addChars(content)
- def endDocument(self):
- """End of the XML document event"""
- self.parent.endDocument()
|