commit Brian's BOM generation scripts so they don't get lost

This commit is contained in:
Dick Hollenbeck 2011-10-20 10:49:07 -05:00
parent 15f0147ece
commit 4c063574ae
9 changed files with 867 additions and 2 deletions

View File

@ -19,6 +19,11 @@ Common
* Push file open semantics down to one of the base frame classes ( likely candidate is
WinEDA_BasicFrame ) so that file open behavior is consistent across all applications.
* Look over Brian's python BOM generation scripts, which are now in
scripts/python/ky and sort them out, and get something into the installation as well.
Code came from Brian in this posting's attachment, which is ky2.zip:
https://lists.launchpad.net/kicad-developers/msg06763.html
but is now in scripts/python/ky temporarily.
CvPCB
@ -48,6 +53,4 @@ E6) Start initial work for changing component library file format to use Dick's
PCBNew
------
Dick:
* Use BOARD_ITEM::MenuIcon() in the onrightclick.cpp

View File

@ -0,0 +1,38 @@
#
# Example python script to generate a BOM from a KiCad generic netlist
#
# Example: Tab delimited list (The same as std output) Ungrouped
#
# Import the KiCad python helper module and the csv formatter
import ky
import csv
import sys
# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
# Create a new csv writer object to use as the output formatter, although we
# are created a tab delimited list instead!
out = csv.writer(f, delimiter='\t', quoting=csv.QUOTE_NONE)
# Output a field delimited header line
out.writerow(['Source:', net.getSource()])
out.writerow(['Date:', net.getDate()])
out.writerow(['Tool:', net.getTool()])
out.writerow(['Component Count:', len(net.components)])
out.writerow(['Ref', 'Value', 'Part', 'Documentation', 'Description', 'Vendor'])
# Output all of the component information
for c in net.components:
out.writerow([c.getRef(), c.getValue(), c.getLib() + "/" + c.getPart(),
c.getDatasheet(), c.getDescription(), c.getField("Vendor")])

View File

@ -0,0 +1,38 @@
#
# Example python script to generate a BOM from a KiCad generic netlist
#
# Example: Ungrouped (One component per row) CSV output
#
# Import the KiCad python helper module
import ky
import csv
import sys
# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
# Create a new csv writer object to use as the output formatter
out = csv.writer(f, delimiter=',', quotechar="\"", quoting=csv.QUOTE_ALL)
# Output a field delimited header line
out.writerow(['Source:', net.getSource()])
out.writerow(['Date:', net.getDate()])
out.writerow(['Tool:', net.getTool()])
out.writerow(['Component Count:', len(net.components)])
out.writerow(['Ref', 'Value', 'Footprint', 'Datasheet', 'Manufacturer', 'Vendor'])
# Output all of the component information (One component per row)
for c in net.components:
out.writerow([c.getRef(), c.getValue(), c.getFootprint(), c.getDatasheet(),
c.getField("Manufacturer"), c.getField("Vendor")])

View File

@ -0,0 +1,51 @@
#
# Example python script to generate a BOM from a KiCad generic netlist
#
# Example: Sorted and Grouped CSV BOM
#
# Import the KiCad python helper module and the csv formatter
import ky
import csv
import sys
# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
# Create a new csv writer object to use as the output formatter
out = csv.writer(f, delimiter=',', quotechar='\"', quoting=csv.QUOTE_ALL)
# Output a set of rows for a header providing general information
out.writerow(['Source:', net.getSource()])
out.writerow(['Date:', net.getDate()])
out.writerow(['Tool:', net.getTool()])
out.writerow(['Component Count:', len(net.components)])
out.writerow(['Ref', 'Qnty', 'Value', 'Part', 'Datasheet', 'Description', 'Vendor'])
# Get all of the components in groups of matching parts + values (see ky.py)
grouped = net.groupComponents()
# Output all of the component information
for group in grouped:
refs = ""
# Add the reference of every component in the group and keep a reference
# to the component so that the other data can be filled in once per group
for component in group:
refs += component.getRef() + ", "
c = component
# Fill in the component groups common data
out.writerow([refs, len(group), c.getValue(), c.getLib() + "/" + c.getPart(), c.getDatasheet(),
c.getDescription(), c.getField("Vendor")])

View File

@ -0,0 +1,78 @@
#
# Example python script to generate a BOM from a KiCad generic netlist
#
# Example: Sorted and Grouped HTML BOM
#
# Import the KiCad python helper module and the csv formatter
import ky
import sys
# Start with a basic html template
html = """
<!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" />
</head>
<body>
<h1><!--SOURCE--></h1>
<p><!--DATE--></p>
<p><!--TOOL--></p>
<p><!--COMPCOUNT--></p>
<table>
<!--TABLEROW-->
</table>
</body>
</html>
"""
# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
# Output a set of rows for a header providing general information
html = html.replace('<!--SOURCE-->', net.getSource())
html = html.replace('<!--DATE-->', net.getDate())
html = html.replace('<!--TOOL-->', net.getTool())
html = html.replace('<!--COMPCOUNT-->', "<b>Component Count:</b>" + \
str(len(net.components)))
row = "<tr><th style='width:640px'>Ref</th>" + "<th>Qnty</th>"
row += "<th>Value</th>" + "<th>Part</th>" + "<th>Datasheet</th>"
row += "<th>Description</th>" + "<th>Vendor</th></tr>"
html = html.replace('<!--TABLEROW-->', row + "<!--TABLEROW-->")
# Get all of the components in groups of matching parts + values (see ky.py)
grouped = net.groupComponents()
# Output all of the component information
for group in grouped:
refs = ""
# Add the reference of every component in the group and keep a reference
# to the component so that the other data can be filled in once per group
for component in group:
refs += component.getRef() + ", "
c = component
row = "<tr><td>" + refs +"</td><td>" + str(len(group))
row += "</td><td>" + c.getValue() + "</td><td>" + c.getLib() + "/"
row += c.getPart() + "</td><td>" + c.getDatasheet() + "</td><td>"
row += c.getDescription() + "</td><td>" + c.getField("Vendor")
row += "</td></tr>"
html = html.replace('<!--TABLEROW-->', row + "<!--TABLEROW-->")
# Print the formatted html to the file
print >> f, html

View File

@ -0,0 +1,113 @@
#
# Example python script to generate a BOM from a KiCad generic netlist
#
# Example: Sorted and Grouped HTML BOM with more advanced grouping
#
# Import the KiCad python helper module and the csv formatter
import ky
import sys
# Start with a basic html template
html = """
<!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>KiCad BOM Example 5</title>
</head>
<body>
<h1><!--SOURCE--></h1>
<p><!--DATE--></p>
<p><!--TOOL--></p>
<p><!--COMPCOUNT--></p>
<table>
<!--TABLEROW-->
</table>
</body>
</html>
"""
def myEqu(self, other):
"""myEqu is a more advanced equivalence function for components which is
used by component grouping. Normal operation is to group components based
on their Value, Library source, and Library part.
In this example of a more advanced equivalency operator we also compare the
custom fields Voltage, Tolerance and Manufacturer as well as the assigned
footprint. If these fields are not used in some parts they will simply be
ignored (they will match as both will be empty strings).
"""
result = True
if self.getValue() != other.getValue():
result = False
elif self.getLib() != other.getLib():
result = False
elif self.getPart() != other.getPart():
result = False
elif self.getFootprint() != other.getFootprint():
result = False
elif self.getField("Tolerance") != other.getField("Tolerance"):
result = False
elif self.getField("Manufacturer") != other.getField("Manufacturer"):
result = False
elif self.getField("Voltage") != other.getField("Voltage"):
result = False
return result
# Override the component equivalence operator - it is important to do this
# before loading the netlist, otherwise all components will have the original
# equivalency operator.
ky.component.__equ__ = myEqu
# Generate an instance of a generic netlist, and load the netlist tree from
# video.tmp. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write too, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
# Output a set of rows for a header providing general information
html = html.replace('<!--SOURCE-->', net.getSource())
html = html.replace('<!--DATE-->', net.getDate())
html = html.replace('<!--TOOL-->', net.getTool())
html = html.replace('<!--COMPCOUNT-->', "<b>Component Count:</b>" + \
str(len(net.components)))
row = "<tr><th style='width:640px'>Ref</th>" + "<th>Qnty</th>"
row += "<th>Value</th>" + "<th>Part</th>" + "<th>Datasheet</th>"
row += "<th>Description</th>" + "<th>Vendor</th></tr>"
html = html.replace('<!--TABLEROW-->', row + "<!--TABLEROW-->")
# Get all of the components in groups of matching parts + values (see ky.py)
grouped = net.groupComponents()
# Output all of the component information
for group in grouped:
refs = ""
# Add the reference of every component in the group and keep a reference
# to the component so that the other data can be filled in once per group
for component in group:
refs += component.getRef() + ", "
c = component
row = "<tr><td>" + refs +"</td><td>" + str(len(group))
row += "</td><td>" + c.getValue() + "</td><td>" + c.getLib() + "/"
row += c.getPart() + "</td><td>" + c.getDatasheet() + "</td><td>"
row += c.getDescription() + "</td><td>" + c.getField("Vendor")
row += "</td></tr>"
html = html.replace('<!--TABLEROW-->', row + "<!--TABLEROW-->")
# Print the formatted html to output file
print >> f, html

View File

@ -0,0 +1,450 @@
#
# 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()

View File

@ -0,0 +1,23 @@
#
# Example python script to generate an equivalent XML document from XML input
#
# Example: Round robin, XML to XML conversion
#
# Import the KiCad python helper module and the csv formatter
import ky
import sys
# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
print >> f, net.formatXML()

View File

@ -0,0 +1,71 @@
#
# Example python script to generate an equivalent XML document from XML input
#
# Example: Round value robin, XML to XML conversion with partial value monging
#
# Import the KiCad python helper module and the csv formatter
import ky
import sys
def checkvalue(self):
"""Check values, and replace with preferred/consistent values"""
ref = self.getRef()
r = ref.split("R")
c = ref.split("C")
v = self.getValue()
# Common to all values - convert decimation if necessary
dec = v.split(",")
if (len(dec) == 2):
newval = dec[0] + "." + dec[1]
self.setValue(newval)
v = self.getValue()
if len(r) == 2 and r[1].isdigit():
# This is a resistor - make values consistent
# If the value is a pure value, add R to the end of the value
if v.isdigit():
i = int(v)
if (i > 1000000):
i = i / 100000
v = str(i) + "M"
if (i > 1000):
i = i / 1000
v = str(i) + "K"
else:
v = str(i) + "R"
self.setValue(v)
else:
# Get the multiplier character
multiplier = v[len(v) - 1]
v = v.strip(multiplier)
v = v.split(".")
if (len(v) == 2):
newval = v[0] + multiplier + v[1]
self.setValue(newval)
v = self.getValue()
# Give components a new method for checking the values (this could easily be a
# Company Part Number generator method instead)
ky.component.checkvalue = checkvalue
# Generate an instance of a generic netlist, and load the netlist tree from
# the command line option. If the file doesn't exist, execution will stop
net = ky.netlist(sys.argv[1])
# Open a file to write to, if the file cannot be opened output to stdout
# instead
try:
f = open(sys.argv[2], 'w')
except IOError:
print >> sys.stderr, __file__, ":", e
f = stdout
for c in net.components:
c.checkvalue()
print >> f, net.formatXML()