diff --git a/TODO.txt b/TODO.txt index b2e5ee911e..485f7b8890 100644 --- a/TODO.txt +++ b/TODO.txt @@ -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 diff --git a/scripts/bom-in-python/ky/bom_example1.py b/scripts/bom-in-python/ky/bom_example1.py new file mode 100644 index 0000000000..e1508baaaf --- /dev/null +++ b/scripts/bom-in-python/ky/bom_example1.py @@ -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")]) diff --git a/scripts/bom-in-python/ky/bom_example2.py b/scripts/bom-in-python/ky/bom_example2.py new file mode 100644 index 0000000000..23fb870f52 --- /dev/null +++ b/scripts/bom-in-python/ky/bom_example2.py @@ -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")]) + diff --git a/scripts/bom-in-python/ky/bom_example3.py b/scripts/bom-in-python/ky/bom_example3.py new file mode 100644 index 0000000000..c5581fa6b7 --- /dev/null +++ b/scripts/bom-in-python/ky/bom_example3.py @@ -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")]) + + diff --git a/scripts/bom-in-python/ky/bom_example4.py b/scripts/bom-in-python/ky/bom_example4.py new file mode 100644 index 0000000000..b438e73a7d --- /dev/null +++ b/scripts/bom-in-python/ky/bom_example4.py @@ -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 = """ + + + + + + +

+

+

+

+ + +
+ + + """ + +# 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('', net.getSource()) +html = html.replace('', net.getDate()) +html = html.replace('', net.getTool()) +html = html.replace('', "Component Count:" + \ + str(len(net.components))) + +row = "Ref" + "Qnty" +row += "Value" + "Part" + "Datasheet" +row += "Description" + "Vendor" + +html = html.replace('', row + "") + +# 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 = "" + refs +"" + str(len(group)) + row += "" + c.getValue() + "" + c.getLib() + "/" + row += c.getPart() + "" + c.getDatasheet() + "" + row += c.getDescription() + "" + c.getField("Vendor") + row += "" + + html = html.replace('', row + "") + +# Print the formatted html to the file +print >> f, html diff --git a/scripts/bom-in-python/ky/bom_example5.py b/scripts/bom-in-python/ky/bom_example5.py new file mode 100644 index 0000000000..a4b8ba8e2d --- /dev/null +++ b/scripts/bom-in-python/ky/bom_example5.py @@ -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 = """ + + + + + KiCad BOM Example 5 + + +

+

+

+

+ + +
+ + + """ + +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('', net.getSource()) +html = html.replace('', net.getDate()) +html = html.replace('', net.getTool()) +html = html.replace('', "Component Count:" + \ + str(len(net.components))) + +row = "Ref" + "Qnty" +row += "Value" + "Part" + "Datasheet" +row += "Description" + "Vendor" + +html = html.replace('', row + "") + +# 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 = "" + refs +"" + str(len(group)) + row += "" + c.getValue() + "" + c.getLib() + "/" + row += c.getPart() + "" + c.getDatasheet() + "" + row += c.getDescription() + "" + c.getField("Vendor") + row += "" + + html = html.replace('', row + "") + +# Print the formatted html to output file +print >> f, html diff --git a/scripts/bom-in-python/ky/ky.py b/scripts/bom-in-python/ky/ky.py new file mode 100644 index 0000000000..ad809a8919 --- /dev/null +++ b/scripts/bom-in-python/ky/ky.py @@ -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 = "\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() diff --git a/scripts/bom-in-python/ky/round_robin.py b/scripts/bom-in-python/ky/round_robin.py new file mode 100644 index 0000000000..9abfe41f7a --- /dev/null +++ b/scripts/bom-in-python/ky/round_robin.py @@ -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() diff --git a/scripts/bom-in-python/ky/round_value_robin.py b/scripts/bom-in-python/ky/round_value_robin.py new file mode 100644 index 0000000000..6099441b6f --- /dev/null +++ b/scripts/bom-in-python/ky/round_value_robin.py @@ -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()