485 lines
14 KiB
Python
485 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
@package
|
|
Generate a HTML BOM list.
|
|
Components are sorted and grouped by value
|
|
Any existing fields are read
|
|
"""
|
|
|
|
|
|
from __future__ import print_function
|
|
import sys
|
|
import xml.sax as sax
|
|
|
|
from bomlib.component import (Component, ComponentGroup)
|
|
|
|
from bomlib.preferences import BomPref
|
|
|
|
# -----</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 = " " * nestLevel
|
|
|
|
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"""
|
|
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:
|
|
return self.chars
|
|
else:
|
|
return self.attributes[attribute]
|
|
except AttributeError:
|
|
return ""
|
|
else:
|
|
return self.chars
|
|
|
|
for child in self.children:
|
|
ret = child.get(elemName, attribute, attrmatch)
|
|
if ret != "":
|
|
return ret
|
|
|
|
return ""
|
|
|
|
|
|
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 getLibName(self):
|
|
return self.element.get("libpart", "lib")
|
|
|
|
def getPartName(self):
|
|
return self.element.get("libpart", "part")
|
|
|
|
# For backwards Compatibility with v4.x only
|
|
def getDescription(self):
|
|
return self.element.get("description")
|
|
|
|
def getDocs(self):
|
|
return self.element.get("docs")
|
|
|
|
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):
|
|
|
|
datasheet = self.getField("Datasheet")
|
|
|
|
if not datasheet or datasheet == "":
|
|
docs = self.getDocs()
|
|
|
|
if "http" in docs or ".pdf" in docs:
|
|
datasheet = docs
|
|
|
|
return 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 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="", prefs=None):
|
|
"""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 not prefs:
|
|
prefs = BomPref() # Default values
|
|
|
|
self.prefs = prefs
|
|
|
|
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 is 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(Component(self._curr_element, prefs=self.prefs))
|
|
|
|
# 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"""
|
|
if (sys.version_info[0] >= 3):
|
|
return self.design.get("date")
|
|
else:
|
|
return self.design.get("date").encode('ascii', 'ignore')
|
|
|
|
def getSource(self):
|
|
"""Return the source string for the design"""
|
|
if (sys.version_info[0] >= 3):
|
|
return self.design.get("source")
|
|
else:
|
|
return self.design.get("source").encode('ascii', 'ignore')
|
|
|
|
def getTool(self):
|
|
"""Return the tool string which was used to create the netlist tree"""
|
|
if (sys.version_info[0] >= 3):
|
|
return self.design.get("tool")
|
|
else:
|
|
return self.design.get("tool").encode('ascii', 'ignore')
|
|
|
|
def getSheet(self):
|
|
return self.design.getChild("sheet")
|
|
|
|
def getSheetDate(self):
|
|
sheet = self.getSheet()
|
|
if sheet is None:
|
|
return ""
|
|
return sheet.get("date")
|
|
|
|
def getVersion(self):
|
|
"""Return the verison of the sheet info"""
|
|
|
|
sheet = self.getSheet()
|
|
|
|
if sheet is None:
|
|
return ""
|
|
|
|
return sheet.get("rev")
|
|
|
|
def getInterestingComponents(self):
|
|
|
|
# Copy out the components
|
|
ret = [c for c in self.components]
|
|
|
|
# Sort first by ref as this makes for easier to read BOM's
|
|
ret.sort(key=lambda g: g.getRef())
|
|
|
|
return ret
|
|
|
|
def groupComponents(self, components):
|
|
|
|
groups = []
|
|
|
|
# Iterate through each component, and test whether a group for these already exists
|
|
for c in components:
|
|
|
|
if self.prefs.useRegex:
|
|
# Skip components if they do not meet regex requirements
|
|
if not c.testRegInclude():
|
|
continue
|
|
if c.testRegExclude():
|
|
continue
|
|
|
|
found = False
|
|
|
|
for g in groups:
|
|
if g.matchComponent(c):
|
|
g.addComponent(c)
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
g = ComponentGroup(prefs=self.prefs) # Pass down the preferences
|
|
g.addComponent(c)
|
|
groups.append(g)
|
|
|
|
# Sort the references within each group
|
|
for g in groups:
|
|
g.sortComponents()
|
|
g.updateFields(self.prefs.useAlt, self.prefs.altWrap)
|
|
|
|
# Sort the groups
|
|
# First priority is the Type of component (e.g. R?, U?, L?)
|
|
groups = sorted(groups, key=lambda g: [g.components[0].getPrefix(), g.components[0].getValue()])
|
|
|
|
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(__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()
|