# # KiCad python module for interpreting generic netlists which can be used # to generate Bills of materials, etc. # # 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 # import sys import xml.sax as sax class component(): """Class for a set of component information""" def __init__(self, element): self.element = element self.libpart = None # Set to true when this component is included in a component group self.grouped = False def __eq__(self, other): """Equlivalency operator, remember this can be easily overloaded""" result = False if self.getValue() == other.getValue(): if self.getLib() == other.getLib(): if self.getPart() == other.getPart(): result = True return result def setPart(self, part): self.libpart = part 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 getRef(self): return self.element.get("comp", "ref") def getFootprint(self): return self.element.get("footprint") def getDatasheet(self): return self.element.get("datasheet") def getLib(self): return self.element.get("libsource", "lib") def getPart(self): return self.element.get("libsource", "part") def getTimestamp(self): return self.element.get("tstamp") def getDescription(self): # When attempting to access the part, we must take care in case the part # cannot be found in the netlist try: d = self.libpart.getDescription() except AttributeError: d = "" return d def getDatasheet(self): # When attempting to access the part, we must take care in case the part # cannot be found in the netlist try: d = self.libpart.getDatasheet() except AttributeError: d = "" return d def getField(self, name): """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 """ field = self.element.get("field", "name", name) if field == "": try: field = self.libpart.getField(name) except AttributeError: field = "" return field class netlistElement(): """Generic netlist element. All elements for a netlist tree which can be used to easily generate various output formats by propogating format requests to all children """ def __init__(self, name, parent=None): self.name = name self.attributes = {} self.parent = parent self.chars = "" self.children = [] self.indent = "" def __str__(self): """String representation of this netlist element """ return (self.name + "[" + self.chars + "]" + " attr:" + str(len(self.attributes[a]))) def formatXML(self, amChild=False): """Return this element formatted as XML Keywords: amChild -- If set to True, the start of document is not returned """ s = "" if not amChild: s = "\n" s += self.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: c.indent += self.indent + " " s += "\n" s += c.formatXML(True) if (len(self.children) > 0): s += "\n" + self.indent if (len(self.children) > 0) or (len(self.chars) > 0): s += "" 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 = """ """ s += "\n" for c in self.children: s += c.formatHTML(True) if not amChild: s += """
" + self.name + "
" + self.chars + "
    " for a in self.attributes: s += "
  • " + a + " = " + self.attributes[a] + "
  • " s += "
""" return s def addAttribute(self, attr, value): """Add an attribute to this element""" 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 setAttribute(self, attr, value): """Set an attributes value - in fact does the same thing as add attribute """ self.attributes[attr] = value def getChild(self, name): """Returns a child element of 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 get(self, element, attribute="", attrmatch=""): """Return the data for either an attribute, or else an element""" if (self.name == element): if attribute != "": if attrmatch != "": if self.attributes[attribute] == attrmatch: return self.chars else: return self.attributes[attribute] else: return self.chars for child in self.children: if child.get(element, attribute, attrmatch) != "": return child.get(element, attribute, attrmatch) return "" 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 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 = netlistElement(name) self._curr_element = self.tree else: self._curr_element = self._curr_element.addChild( netlistElement(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(component(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(part(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.getPart() == c.getPart() and p.getLib() == c.getLib(): c.setPart(p) 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 groupComponents(self): """Return a list of component lists. Components are grouped together when the value, library and part identifiers match """ groups = [] # Make sure to start off will all components ungrouped to begin with for c in self.components: c.grouped = False # Group components based on the value, library and part identifiers for c in self.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 self.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) # Each group is a list of components, we need to sort each list first # to get them in order as this makes for easier to read BOM's for g in groups: g = sorted(g, key=lambda g: g.getRef()) # Finally, sort the groups to order the references alphabetically groups = sorted(groups, key=lambda group: group[0].getRef()) return groups 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 >> sys.stderr, __file__, ":", e sys.exit(-1) class part(): """Class for a library part""" def __init__(self, part): # The part is a reference to a libpart generic netlist element self.element = part def __str__(self): # simply print the generic netlist element associated with this part return str(self.element) def getDatasheet(self): return self.element.get("docs") def getLib(self): return self.element.get("libpart", "lib") def getPart(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) class _gNetReader(sax.handler.ContentHandler): """SAX kicad generic netlist content handler - passes most of the work back to the gNetlist 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()