kicad/scripts/bom-in-python/ky_generic_netlist_reader.py

451 lines
14 KiB
Python

#
# 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 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\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 += "</" + 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"""
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()