move everything to 'old' directory before merge

This commit is contained in:
2022-09-20 18:26:15 -06:00
parent 58b6ccd61a
commit 7a1d9adc79
172 changed files with 2 additions and 2 deletions

View File

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
from bomlib.csv_writer import WriteCSV
from bomlib.xml_writer import WriteXML
from bomlib.html_writer import WriteHTML
from bomlib.xlsx_writer import WriteXLSX
import bomlib.columns as columns
from bomlib.preferences import BomPref
import os
import shutil
def TmpFileCopy(filename, fmt):
# Make a tmp copy of a given file
filename = os.path.abspath(filename)
if os.path.exists(filename) and os.path.isfile(filename):
shutil.copyfile(filename, fmt.replace("%O", filename))
def WriteBoM(filename, groups, net, headings=columns.ColumnList._COLUMNS_DEFAULT, prefs=None):
"""
Write BoM to file
filename = output file path
groups = [list of ComponentGroup groups]
headings = [list of headings to display in the BoM file]
prefs = BomPref object
"""
filename = os.path.abspath(filename)
# No preferences supplied, use defaults
if not prefs:
prefs = BomPref()
# Remove any headings that appear in the ignore[] list
headings = [h for h in headings if not h.lower() in [i.lower() for i in prefs.ignore]]
# If no extension is given, assume .csv (and append!)
if len(filename.split('.')) < 2:
filename += ".csv"
# Make a temporary copy of the output file
if prefs.backup is not False:
TmpFileCopy(filename, prefs.backup)
ext = filename.split('.')[-1].lower()
result = False
# CSV file writing
if ext in ["csv", "tsv", "txt"]:
if WriteCSV(filename, groups, net, headings, prefs):
print("CSV Output -> {fn}".format(fn=filename))
result = True
else:
print("Error writing CSV output")
elif ext in ["htm", "html"]:
if WriteHTML(filename, groups, net, headings, prefs):
print("HTML Output -> {fn}".format(fn=filename))
result = True
else:
print("Error writing HTML output")
elif ext in ["xml"]:
if WriteXML(filename, groups, net, headings, prefs):
print("XML Output -> {fn}".format(fn=filename))
result = True
else:
print("Error writing XML output")
elif ext in ["xlsx"] and prefs.xlsxwriter_available:
if WriteXLSX(filename, groups, net, headings, prefs):
print("XLSX Output -> {fn}".format(fn=filename))
result = True
else:
print("Error writing XLSX output")
else:
print("Unsupported file extension: {ext}".format(ext=ext))
return result

View File

@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
class ColumnList:
# Default columns (immutable)
COL_REFERENCE = 'References'
COL_DESCRIPTION = 'Description'
COL_VALUE = 'Value'
COL_FP = 'Footprint'
COL_FP_LIB = 'Footprint Lib'
COL_PART = 'Part'
COL_PART_LIB = 'Part Lib'
COL_DATASHEET = 'Datasheet'
# Default columns for groups
COL_GRP_QUANTITY = 'Quantity Per PCB'
COL_GRP_TOTAL_COST = 'Total Cost'
COL_GRP_BUILD_QUANTITY = 'Build Quantity'
# Generated columns
_COLUMNS_GEN = [
COL_GRP_QUANTITY,
COL_GRP_BUILD_QUANTITY,
]
# Default columns
_COLUMNS_DEFAULT = [
COL_DESCRIPTION,
COL_PART,
COL_PART_LIB,
COL_REFERENCE,
COL_VALUE,
COL_FP,
COL_FP_LIB,
COL_GRP_QUANTITY,
COL_GRP_BUILD_QUANTITY,
COL_DATASHEET
]
# Default columns
# These columns are 'immutable'
_COLUMNS_PROTECTED = [
COL_REFERENCE,
COL_GRP_QUANTITY,
COL_VALUE,
COL_PART,
COL_PART_LIB,
COL_DESCRIPTION,
COL_DATASHEET,
COL_FP,
COL_FP_LIB
]
def __str__(self):
return " ".join(map(str, self.columns))
def __repr__(self):
return self.__str__()
def __init__(self, cols=_COLUMNS_DEFAULT):
self.columns = []
# Make a copy of the supplied columns
for col in cols:
self.AddColumn(col)
def _hasColumn(self, col):
# Col can either be <str> or <Column>
return col.lower() in [c.lower() for c in self.columns]
"""
Remove a column from the list. Specify either the heading or the index
"""
def RemoveColumn(self, col):
if type(col) is str:
self.RemoveColumnByName(col)
elif type(col) is int and col >= 0 and col < len(self.columns):
self.RemoveColumnByName(self.columns[col])
def RemoveColumnByName(self, name):
# First check if this is in an immutable colum
if name in self._COLUMNS_PROTECTED:
return
# Column does not exist, return
if name not in self.columns:
return
try:
index = self.columns.index(name)
del self.columns[index]
except ValueError:
return
# Add a new column (if it doesn't already exist!)
def AddColumn(self, col, index=None):
# Already exists?
if self._hasColumn(col):
return
if type(index) is not int or index < 0 or index >= len(self.columns):
self.columns.append(col)
# Otherwise, splice the new column in
else:
self.columns = self.columns[0:index] + [col] + self.columns[index:]
if __name__ == '__main__':
c = ColumnList()
c.AddColumn("Test1")
c.AddColumn("Test1")
c.AddColumn("Test2")
c.AddColumn("Test3")
c.AddColumn("Test4")
c.AddColumn("Test2")
c.RemoveColumn("Test2")
c.RemoveColumn("Part")
c.RemoveColumn(2)
c.RemoveColumn(5)

View File

@@ -0,0 +1,576 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from bomlib.columns import ColumnList
from bomlib.preferences import BomPref
import bomlib.units as units
from bomlib.sort import natural_sort
import re
import sys
DNF = [
"dnf",
"dnl",
"dnp",
"do not fit",
"do not place",
"do not load",
"nofit",
"nostuff",
"noplace",
"noload",
"not fitted",
"not loaded",
"not placed",
"no stuff",
]
class Component():
"""Class for a component, aka 'comp' in the xml netlist file.
This component class is implemented by wrapping an xmlElement instance
with accessors. The xmlElement is held in field 'element'.
"""
def __init__(self, xml_element, prefs=None):
self.element = xml_element
self.libpart = None
if not prefs:
prefs = BomPref()
self.prefs = prefs
# Set to true when this component is included in a component group
self.grouped = False
# Compare the value of this part, to the value of another part (see if they match)
def compareValue(self, other):
# Simple string comparison
if self.getValue().lower() == other.getValue().lower():
return True
# Otherwise, perform a more complicated value comparison
if units.compareValues(self.getValue(), other.getValue()):
return True
# Ignore value if both components are connectors
if self.prefs.groupConnectors:
if 'connector' in self.getLibName().lower() and 'connector' in other.getLibName().lower():
return True
# No match, return False
return False
# Determine if two parts have the same name
def comparePartName(self, other):
pn1 = self.getPartName().lower()
pn2 = other.getPartName().lower()
# Simple direct match
if pn1 == pn2:
return True
# Compare part aliases e.g. "c" to "c_small"
for alias in self.prefs.aliases:
if pn1 in alias and pn2 in alias:
return True
return False
def compareField(self, other, field):
this_field = self.getField(field).lower()
other_field = other.getField(field).lower()
# If blank comparisons are allowed
if this_field == "" or other_field == "":
if not self.prefs.mergeBlankFields:
return False
if this_field == other_field:
return True
return False
def __eq__(self, other):
"""
Equivalency operator is used to determine if two parts are 'equal'
"""
# 'fitted' value must be the same for both parts
if self.isFitted() != other.isFitted():
return False
if len(self.prefs.groups) == 0:
return False
for c in self.prefs.groups:
# Perform special matches
if c.lower() == ColumnList.COL_VALUE.lower():
if not self.compareValue(other):
return False
# Match part name
elif c.lower() == ColumnList.COL_PART.lower():
if not self.comparePartName(other):
return False
# Generic match
elif not self.compareField(other, c):
return False
return True
def setLibPart(self, part):
self.libpart = part
def getPrefix(self):
"""
Get the reference prefix
e.g. if this component has a reference U12, will return "U"
"""
prefix = ""
for c in self.getRef():
if c.isalpha():
prefix += c
else:
break
return prefix
def getSuffix(self):
"""
Return the reference suffix #
e.g. if this component has a reference U12, will return "12"
"""
suffix = ""
for c in self.getRef():
if c.isalpha():
suffix = ""
else:
suffix += c
return int(suffix)
def getLibPart(self):
return self.libpart
def getPartName(self):
return self.element.get("libsource", "part")
def getLibName(self):
return self.element.get("libsource", "lib")
def getDescription(self):
try:
return self.element.get("libsource", "description")
except:
# Compatibility with old KiCad versions (4.x)
ret = self.element.get("field", "name", "description")
if ret == "":
ret = self.libpart.getDescription()
return ret
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 getField(self, name, ignoreCase=True, libraryToo=True):
"""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
libraryToo -- look in the libpart's fields for the same name if not found
in component itself
"""
fp = self.getFootprint().split(":")
if name.lower() == ColumnList.COL_REFERENCE.lower():
return self.getRef().strip()
elif name.lower() == ColumnList.COL_DESCRIPTION.lower():
return self.getDescription().strip()
elif name.lower() == ColumnList.COL_DATASHEET.lower():
return self.getDatasheet().strip()
# Footprint library is first element
elif name.lower() == ColumnList.COL_FP_LIB.lower():
if len(fp) > 1:
return fp[0].strip()
else:
# Explicit empty return
return ""
elif name.lower() == ColumnList.COL_FP.lower():
if len(fp) > 1:
return fp[1].strip()
elif len(fp) == 1:
return fp[0]
else:
return ""
elif name.lower() == ColumnList.COL_VALUE.lower():
return self.getValue().strip()
elif name.lower() == ColumnList.COL_PART.lower():
return self.getPartName().strip()
elif name.lower() == ColumnList.COL_PART_LIB.lower():
return self.getLibName().strip()
# Other fields (case insensitive)
for f in self.getFieldNames():
if f.lower() == name.lower():
field = self.element.get("field", "name", f)
if field == "" and libraryToo:
field = self.libpart.getField(f)
return field.strip()
# Could not find a matching field
return ""
def getFieldNames(self):
"""Return a list of field names in play for this component. Mandatory
fields are not included, and they are: Value, Footprint, Datasheet, Ref.
The netlist format only includes fields with non-empty values. So if a field
is empty, it will not be present in the returned list.
"""
fieldNames = []
fields = self.element.getChild('fields')
if fields:
for f in fields.getChildren():
fieldNames.append(f.get('field', 'name'))
return fieldNames
def getRef(self):
return self.element.get("comp", "ref")
# Determine if a component is FITTED or not
def isFitted(self):
check = self.getField(self.prefs.configField).lower()
# Check the value field first
if self.getValue().lower() in DNF:
return False
# Empty value means part is fitted
if check == "":
return True
opts = check.lower().split(",")
exclude = False
include = True
for opt in opts:
opt = opt.strip()
# Any option containing a DNF is not fitted
if opt in DNF:
exclude = True
break
# Options that start with '-' are explicitly removed from certain configurations
if opt.startswith("-") and str(opt[1:]) in [str(cfg) for cfg in self.prefs.pcbConfig]:
exclude = True
break
if opt.startswith("+"):
include = include or opt[1:] in [str(cfg) for cfg in self.prefs.pcbConfig]
return include and not exclude
# Test if this part should be included, based on any regex expressions provided in the preferences
def testRegExclude(self):
for reg in self.prefs.regExcludes:
if type(reg) == list and len(reg) == 2:
field_name, regex = reg
field_value = self.getField(field_name)
# Attempt unicode escaping...
# Filthy hack
try:
regex = regex.decode("unicode_escape")
except:
pass
if re.search(regex, field_value, flags=re.IGNORECASE) is not None:
if self.prefs.verbose:
print("Excluding '{ref}': Field '{field}' ({value}) matched '{reg}'".format(
ref=self.getRef(),
field=field_name,
value=field_value,
reg=regex).encode('utf-8'))
# Found a match
return True
# Default, could not find any matches
return False
def testRegInclude(self):
if len(self.prefs.regIncludes) == 0: # Nothing to match against
return True
for reg in self.prefs.regIncludes:
if type(reg) == list and len(reg) == 2:
field_name, regex = reg
field_value = self.getField(field_name)
print(field_name, field_value, regex)
if re.search(regex, field_value, flags=re.IGNORECASE) is not None:
if self.prefs.verbose:
print("")
# Found a match
return True
# Default, could not find a match
return False
def getFootprint(self, libraryToo=True):
ret = self.element.get("footprint")
if ret == "" and libraryToo:
if self.libpart:
ret = self.libpart.getFootprint()
return ret
def getDatasheet(self, libraryToo=True):
ret = self.element.get("datasheet")
if ret == "" and libraryToo:
ret = self.libpart.getDatasheet()
return ret
def getTimestamp(self):
return self.element.get("tstamp")
class joiner:
def __init__(self):
self.stack = []
def add(self, P, N):
if self.stack == []:
self.stack.append(((P, N), (P, N)))
return
S, E = self.stack[-1]
if N == E[1] + 1:
self.stack[-1] = (S, (P, N))
else:
self.stack.append(((P, N), (P, N)))
def flush(self, sep, N=None, dash='-'):
refstr = u''
c = 0
for Q in self.stack:
if bool(N) and c != 0 and c % N == 0:
refstr += u'\n'
elif c != 0:
refstr += sep
S, E = Q
if S == E:
refstr += "%s%d" % S
c += 1
else:
# Do we have space?
if bool(N) and (c + 1) % N == 0:
refstr += u'\n'
c += 1
refstr += "%s%d%s%s%d" % (S[0], S[1], dash, E[0], E[1])
c += 2
return refstr
class ComponentGroup():
"""
Initialize the group with no components, and default fields
"""
def __init__(self, prefs=None):
self.components = []
self.fields = dict.fromkeys(ColumnList._COLUMNS_DEFAULT) # Columns loaded from KiCad
if not prefs:
prefs = BomPref()
self.prefs = prefs
def getField(self, field):
if field not in self.fields.keys():
return ""
if not self.fields[field]:
return ""
return u''.join((self.fields[field]))
def getCount(self):
return len(self.components)
# Test if a given component fits in this group
def matchComponent(self, c):
if len(self.components) == 0:
return True
if c == self.components[0]:
return True
return False
def containsComponent(self, c):
# Test if a given component is already contained in this grop
if not self.matchComponent(c):
return False
for comp in self.components:
if comp.getRef() == c.getRef():
return True
return False
def addComponent(self, c):
# Add a component to the group
if len(self.components) == 0:
self.components.append(c)
elif self.containsComponent(c):
return
elif self.matchComponent(c):
self.components.append(c)
def isFitted(self):
return any([c.isFitted() for c in self.components])
def getRefs(self):
# Return a list of the components
return " ".join([c.getRef() for c in self.components])
def getAltRefs(self, wrapN=None):
S = joiner()
for n in self.components:
P, N = (n.getPrefix(), n.getSuffix())
S.add(P, N)
return S.flush(' ', N=wrapN)
# Sort the components in correct order
def sortComponents(self):
self.components = sorted(self.components, key=lambda c: natural_sort(c.getRef()))
# Update a given field, based on some rules and such
def updateField(self, field, fieldData):
# Protected fields cannot be overwritten
if field in ColumnList._COLUMNS_PROTECTED:
return
if field is None or field == "":
return
elif fieldData == "" or fieldData is None:
return
if (field not in self.fields.keys()) or (self.fields[field] is None) or (self.fields[field] == ""):
self.fields[field] = fieldData
elif fieldData.lower() in self.fields[field].lower():
return
else:
print("Field conflict: ({refs}) [{name}] : '{flds}' <- '{fld}'".format(
refs=self.getRefs(),
name=field,
flds=self.fields[field],
fld=fieldData).encode('utf-8'))
self.fields[field] += " " + fieldData
def updateFields(self, usealt=False, wrapN=None):
for c in self.components:
for f in c.getFieldNames():
# These columns are handled explicitly below
if f in ColumnList._COLUMNS_PROTECTED:
continue
self.updateField(f, c.getField(f))
# Update 'global' fields
if usealt:
self.fields[ColumnList.COL_REFERENCE] = self.getAltRefs(wrapN)
else:
self.fields[ColumnList.COL_REFERENCE] = self.getRefs()
q = self.getCount()
self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}".format(
n=q,
dnf=" (DNF)" if not self.isFitted() else "")
self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.boards) if self.isFitted() else "0"
self.fields[ColumnList.COL_VALUE] = self.components[0].getValue()
self.fields[ColumnList.COL_PART] = self.components[0].getPartName()
self.fields[ColumnList.COL_PART_LIB] = self.components[0].getLibName()
self.fields[ColumnList.COL_DESCRIPTION] = self.components[0].getDescription()
self.fields[ColumnList.COL_DATASHEET] = self.components[0].getDatasheet()
# Footprint field requires special attention
fp = self.components[0].getFootprint().split(":")
if len(fp) >= 2:
self.fields[ColumnList.COL_FP_LIB] = fp[0]
self.fields[ColumnList.COL_FP] = fp[1]
elif len(fp) == 1:
self.fields[ColumnList.COL_FP_LIB] = ""
self.fields[ColumnList.COL_FP] = fp[0]
else:
self.fields[ColumnList.COL_FP_LIB] = ""
self.fields[ColumnList.COL_FP] = ""
# Return a dict of the KiCad data based on the supplied columns
# NOW WITH UNICODE SUPPORT!
def getRow(self, columns):
row = []
for key in columns:
val = self.getField(key)
if val is None:
val = ""
else:
val = u'' + val
if sys.version_info[0] < 3:
val = val.encode('utf-8')
row.append(val)
return row

View File

@@ -0,0 +1,92 @@
# _*_ coding:latin-1 _*_
import csv
import os
import sys
def WriteCSV(filename, groups, net, headings, prefs):
"""
Write BoM out to a CSV file
filename = path to output file (must be a .csv, .txt or .tsv file)
groups = [list of ComponentGroup groups]
net = netlist object
headings = [list of headings to display in the BoM file]
prefs = BomPref object
"""
filename = os.path.abspath(filename)
# Delimeter is assumed from file extension
# Override delimiter if separator specified
if prefs.separatorCSV is not None:
delimiter = prefs.separatorCSV
else:
if filename.endswith(".csv"):
delimiter = ","
elif filename.endswith(".tsv") or filename.endswith(".txt"):
delimiter = "\t"
else:
return False
nGroups = len(groups)
nTotal = sum([g.getCount() for g in groups])
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
nBuild = nFitted * prefs.boards
if (sys.version_info[0] >= 3):
f = open(filename, "w", encoding='utf-8')
else:
f = open(filename, "w")
writer = csv.writer(f, delimiter=delimiter, lineterminator="\n")
if not prefs.hideHeaders:
if prefs.numberRows:
writer.writerow(["Component"] + headings)
else:
writer.writerow(headings)
count = 0
rowCount = 1
for group in groups:
if prefs.ignoreDNF and not group.isFitted():
continue
row = group.getRow(headings)
if prefs.numberRows:
row = [str(rowCount)] + row
# Deal with unicode characters
# Row = [el.decode('latin-1') for el in row]
writer.writerow(row)
try:
count += group.getCount()
except:
pass
rowCount += 1
if not prefs.hidePcbInfo:
# Add some blank rows
for i in range(5):
writer.writerow([])
writer.writerow(["Component Groups:", nGroups])
writer.writerow(["Component Count:", nTotal])
writer.writerow(["Fitted Components:", nFitted])
writer.writerow(["Number of PCBs:", prefs.boards])
writer.writerow(["Total components:", nBuild])
writer.writerow(["Schematic Version:", net.getVersion()])
writer.writerow(["Schematic Date:", net.getSheetDate()])
writer.writerow(["PCB Variant:", ' + '.join(prefs.pcbConfig)])
writer.writerow(["BoM Date:", net.getDate()])
writer.writerow(["Schematic Source:", net.getSource()])
writer.writerow(["KiCad Version:", net.getTool()])
f.close()
return True

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
from bomlib.component import ColumnList
BG_GEN = "#E6FFEE"
BG_KICAD = "#FFE6B3"
BG_USER = "#E6F9FF"
BG_EMPTY = "#FF8080"
def bgColor(col):
""" Return a background color for a given column title """
# Auto-generated columns
if col in ColumnList._COLUMNS_GEN:
return BG_GEN
# KiCad protected columns
elif col in ColumnList._COLUMNS_PROTECTED:
return BG_KICAD
# Additional user columns
else:
return BG_USER
def link(text):
for t in ["http", "https", "ftp", "www"]:
if text.startswith(t):
return '<a href="{t}">{t}</a>'.format(t=text)
return text
def WriteHTML(filename, groups, net, headings, prefs):
"""
Write BoM out to a HTML file
filename = path to output file (must be a .htm or .html file)
groups = [list of ComponentGroup groups]
net = netlist object
headings = [list of headings to display in the BoM file]
prefs = BomPref object
"""
if not filename.endswith(".html") and not filename.endswith(".htm"):
print("{fn} is not a valid html file".format(fn=filename))
return False
nGroups = len(groups)
nTotal = sum([g.getCount() for g in groups])
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
nBuild = nFitted * prefs.boards
with open(filename, "w") as html:
# HTML Header
html.write("<html>\n")
html.write("<head>\n")
html.write('\t<meta charset="UTF-8">\n') # UTF-8 encoding for unicode support
html.write("</head>\n")
html.write("<body>\n")
# PCB info
if not prefs.hideHeaders:
html.write("<h2>KiBoM PCB Bill of Materials</h2>\n")
if not prefs.hidePcbInfo:
html.write('<table border="1">\n')
html.write("<tr><td>Source File</td><td>{source}</td></tr>\n".format(source=net.getSource()))
html.write("<tr><td>BoM Date</td><td>{date}</td></tr>\n".format(date=net.getDate()))
html.write("<tr><td>Schematic Version</td><td>{version}</td></tr>\n".format(version=net.getVersion()))
html.write("<tr><td>Schematic Date</td><td>{date}</td></tr>\n".format(date=net.getSheetDate()))
html.write("<tr><td>PCB Variant</td><td>{variant}</td></tr>\n".format(variant=', '.join(prefs.pcbConfig)))
html.write("<tr><td>KiCad Version</td><td>{version}</td></tr>\n".format(version=net.getTool()))
html.write("<tr><td>Component Groups</td><td>{n}</td></tr>\n".format(n=nGroups))
html.write("<tr><td>Component Count (per PCB)</td><td>{n}</td></tr>\n".format(n=nTotal))
html.write("<tr><td>Fitted Components (per PCB)</td><td>{n}</td></tr>\n".format(n=nFitted))
html.write("<tr><td>Number of PCBs</td><td>{n}</td></tr>\n".format(n=prefs.boards))
html.write("<tr><td>Total Component Count<br>(for {n} PCBs)</td><td>{t}</td></tr>\n".format(n=prefs.boards, t=nBuild))
html.write("</table>\n")
html.write("<br>\n")
if not prefs.hideHeaders:
html.write("<h2>Component Groups</h2>\n")
html.write('<p style="background-color: {bg}">KiCad Fields (default)</p>\n'.format(bg=BG_KICAD))
html.write('<p style="background-color: {bg}">Generated Fields</p>\n'.format(bg=BG_GEN))
html.write('<p style="background-color: {bg}">User Fields</p>\n'.format(bg=BG_USER))
html.write('<p style="background-color: {bg}">Empty Fields</p>\n'.format(bg=BG_EMPTY))
# Component groups
html.write('<table border="1">\n')
# Row titles:
html.write("<tr>\n")
if prefs.numberRows:
html.write("\t<th></th>\n")
for i, h in enumerate(headings):
# Cell background color
bg = bgColor(h)
html.write('\t<th align="center"{bg}>{h}</th>\n'.format(
h=h,
bg=' bgcolor="{c}"'.format(c=bg) if bg else '')
)
html.write("</tr>\n")
rowCount = 0
for i, group in enumerate(groups):
if prefs.ignoreDNF and not group.isFitted():
continue
row = group.getRow(headings)
rowCount += 1
html.write("<tr>\n")
if prefs.numberRows:
html.write('\t<td align="center">{n}</td>\n'.format(n=rowCount))
for n, r in enumerate(row):
if (len(r) == 0) or (r.strip() == "~"):
bg = BG_EMPTY
else:
bg = bgColor(headings[n])
html.write('\t<td align="center"{bg}>{val}</td>\n'.format(bg=' bgcolor={c}'.format(c=bg) if bg else '', val=link(r)))
html.write("</tr>\n")
html.write("</table>\n")
html.write("<br><br>\n")
html.write("</body></html>")
return True

View File

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

View File

@@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import re
import os
from bomlib.columns import ColumnList
# Check python version to determine which version of ConfirParser to import
if sys.version_info.major >= 3:
import configparser as ConfigParser
else:
import ConfigParser
class BomPref:
SECTION_IGNORE = "IGNORE_COLUMNS"
SECTION_COLUMN_ORDER = "COLUMN_ORDER"
SECTION_GENERAL = "BOM_OPTIONS"
SECTION_ALIASES = "COMPONENT_ALIASES"
SECTION_GROUPING_FIELDS = "GROUP_FIELDS"
SECTION_REGEXCLUDES = "REGEX_EXCLUDE"
SECTION_REGINCLUDES = "REGEX_INCLUDE"
OPT_PCB_CONFIG = "pcb_configuration"
OPT_NUMBER_ROWS = "number_rows"
OPT_GROUP_CONN = "group_connectors"
OPT_USE_REGEX = "test_regex"
OPT_USE_ALT = "use_alt"
OPT_ALT_WRAP = "alt_wrap"
OPT_MERGE_BLANK = "merge_blank_fields"
OPT_IGNORE_DNF = "ignore_dnf"
OPT_BACKUP = "make_backup"
OPT_OUTPUT_FILE_NAME = "output_file_name"
OPT_VARIANT_FILE_NAME_FORMAT = "variant_file_name_format"
OPT_DEFAULT_BOARDS = "number_boards"
OPT_DEFAULT_PCBCONFIG = "board_variant"
OPT_CONFIG_FIELD = "fit_field"
OPT_HIDE_HEADERS = "hide_headers"
OPT_HIDE_PCB_INFO = "hide_pcb_info"
def __init__(self):
# List of headings to ignore in BoM generation
self.ignore = [
ColumnList.COL_PART_LIB,
ColumnList.COL_FP_LIB,
]
self.corder = ColumnList._COLUMNS_DEFAULT
self.useAlt = False # Use alternate reference representation
self.altWrap = None # Wrap to n items when using alt representation
self.ignoreDNF = True # Ignore rows for do-not-fit parts
self.numberRows = True # Add row-numbers to BoM output
self.groupConnectors = True # Group connectors and ignore component value
self.useRegex = True # Test various columns with regex
self.boards = 1 # Quantity of boards to be made
self.mergeBlankFields = True # Blanks fields will be merged when possible
self.hideHeaders = False
self.hidePcbInfo = False
self.verbose = False # By default, is not verbose
self.configField = "Config" # Default field used for part fitting config
self.pcbConfig = ["default"]
self.backup = "%O.tmp"
self.separatorCSV = None
self.outputFileName = "%O_bom_%v%V"
self.variantFileNameFormat = "_(%V)"
self.xlsxwriter_available = False
self.xlsxwriter2_available = False
# Default fields used to group components
self.groups = [
ColumnList.COL_PART,
ColumnList.COL_PART_LIB,
ColumnList.COL_VALUE,
ColumnList.COL_FP,
ColumnList.COL_FP_LIB,
# User can add custom grouping columns in bom.ini
]
self.regIncludes = [] # None by default
self.regExcludes = [
[ColumnList.COL_REFERENCE, '^TP[0-9]*'],
[ColumnList.COL_REFERENCE, '^FID'],
[ColumnList.COL_PART, 'mount.*hole'],
[ColumnList.COL_PART, 'solder.*bridge'],
[ColumnList.COL_PART, 'test.*point'],
[ColumnList.COL_FP, 'test.*point'],
[ColumnList.COL_FP, 'mount.*hole'],
[ColumnList.COL_FP, 'fiducial'],
]
# Default component groupings
self.aliases = [
["c", "c_small", "cap", "capacitor"],
["r", "r_small", "res", "resistor"],
["sw", "switch"],
["l", "l_small", "inductor"],
["zener", "zenersmall"],
["d", "diode", "d_small"]
]
# Check an option within the SECTION_GENERAL group
def checkOption(self, parser, opt, default=False):
if parser.has_option(self.SECTION_GENERAL, opt):
return parser.get(self.SECTION_GENERAL, opt).lower() in ["1", "true", "yes"]
else:
return default
def checkInt(self, parser, opt, default=False):
if parser.has_option(self.SECTION_GENERAL, opt):
return int(parser.get(self.SECTION_GENERAL, opt).lower())
else:
return default
# Read KiBOM preferences from file
def Read(self, file, verbose=False):
file = os.path.abspath(file)
if not os.path.exists(file) or not os.path.isfile(file):
print("{f} is not a valid file!".format(f=file))
return
cf = ConfigParser.RawConfigParser(allow_no_value=True)
cf.optionxform = str
cf.read(file)
# Read general options
if self.SECTION_GENERAL in cf.sections():
self.ignoreDNF = self.checkOption(cf, self.OPT_IGNORE_DNF, default=True)
self.useAlt = self.checkOption(cf, self.OPT_USE_ALT, default=False)
self.altWrap = self.checkInt(cf, self.OPT_ALT_WRAP, default=None)
self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True)
self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True)
self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True)
self.mergeBlankFields = self.checkOption(cf, self.OPT_MERGE_BLANK, default=True)
self.outputFileName = cf.get(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME)
self.variantFileNameFormat = cf.get(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT)
if cf.has_option(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD):
self.configField = cf.get(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD)
if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS):
self.boards = self.checkInt(cf, self.OPT_DEFAULT_BOARDS, default=None)
if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG):
self.pcbConfig = cf.get(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG).strip().split(",")
if cf.has_option(self.SECTION_GENERAL, self.OPT_BACKUP):
self.backup = cf.get(self.SECTION_GENERAL, self.OPT_BACKUP)
else:
self.backup = False
if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS):
self.hideHeaders = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS) == '1'
if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO):
self.hidePcbInfo = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO) == '1'
# Read out grouping colums
if self.SECTION_GROUPING_FIELDS in cf.sections():
self.groups = [i for i in cf.options(self.SECTION_GROUPING_FIELDS)]
# Read out ignored-rows
if self.SECTION_IGNORE in cf.sections():
self.ignore = [i for i in cf.options(self.SECTION_IGNORE)]
# Read out column order
if self.SECTION_COLUMN_ORDER in cf.sections():
self.corder = [i for i in cf.options(self.SECTION_COLUMN_ORDER)]
# Read out component aliases
if self.SECTION_ALIASES in cf.sections():
self.aliases = [re.split('[ \t]+', a) for a in cf.options(self.SECTION_ALIASES)]
if self.SECTION_REGEXCLUDES in cf.sections():
self.regExcludes = []
for pair in cf.options(self.SECTION_REGEXCLUDES):
if len(re.split('[ \t]+', pair)) == 2:
self.regExcludes.append(re.split('[ \t]+', pair))
if self.SECTION_REGINCLUDES in cf.sections():
self.regIncludes = []
for pair in cf.options(self.SECTION_REGINCLUDES):
if len(re.split('[ \t]+', pair)) == 2:
self.regIncludes.append(re.split('[ \t]+', pair))
# Add an option to the SECTION_GENRAL group
def addOption(self, parser, opt, value, comment=None):
if comment:
if not comment.startswith(";"):
comment = "; " + comment
parser.set(self.SECTION_GENERAL, comment)
parser.set(self.SECTION_GENERAL, opt, "1" if value else "0")
# Write KiBOM preferences to file
def Write(self, file):
file = os.path.abspath(file)
cf = ConfigParser.RawConfigParser(allow_no_value=True)
cf.optionxform = str
cf.add_section(self.SECTION_GENERAL)
cf.set(self.SECTION_GENERAL, "; General BoM options here")
self.addOption(cf, self.OPT_IGNORE_DNF, self.ignoreDNF, comment="If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF))
self.addOption(cf, self.OPT_USE_ALT, self.useAlt, comment="If '{opt}' option is set to 1, grouped references will be printed in the alternate compressed style eg: R1-R7,R18".format(opt=self.OPT_USE_ALT))
self.addOption(cf, self.OPT_ALT_WRAP, self.altWrap, comment="If '{opt}' option is set to and integer N, the references field will wrap after N entries are printed".format(opt=self.OPT_ALT_WRAP))
self.addOption(cf, self.OPT_NUMBER_ROWS, self.numberRows, comment="If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS))
self.addOption(cf, self.OPT_GROUP_CONN, self.groupConnectors, comment="If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN))
self.addOption(cf, self.OPT_USE_REGEX, self.useRegex, comment="If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX))
self.addOption(cf, self.OPT_MERGE_BLANK, self.mergeBlankFields, comment="If '{opt}' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible".format(opt=self.OPT_MERGE_BLANK))
cf.set(self.SECTION_GENERAL, "; Specify output file name format, %O is the defined output name, %v is the version, %V is the variant name which will be ammended according to 'variant_file_name_format'.")
cf.set(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME, self.outputFileName)
cf.set(self.SECTION_GENERAL, "; Specify the variant file name format, this is a unique field as the variant is not always used/specified. When it is unused you will want to strip all of this.")
cf.set(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT, self.variantFileNameFormat)
cf.set(self.SECTION_GENERAL, '; Field name used to determine if a particular part is to be fitted')
cf.set(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD, self.configField)
cf.set(self.SECTION_GENERAL, '; Make a backup of the bom before generating the new one, using the following template')
cf.set(self.SECTION_GENERAL, self.OPT_BACKUP, self.backup)
cf.set(self.SECTION_GENERAL, '; Default number of boards to produce if none given on CLI with -n')
cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS, self.boards)
cf.set(self.SECTION_GENERAL, '; Default PCB variant if none given on CLI with -r')
cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG, self.pcbConfig)
cf.set(self.SECTION_GENERAL, '; Whether to hide headers from output file')
cf.set(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS, self.hideHeaders)
cf.set(self.SECTION_GENERAL, '; Whether to hide PCB info from output file')
cf.set(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO, self.hidePcbInfo)
cf.add_section(self.SECTION_IGNORE)
cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM")
cf.set(self.SECTION_IGNORE, "; Titles are case-insensitive")
for i in self.ignore:
cf.set(self.SECTION_IGNORE, i)
cf.add_section(self.SECTION_COLUMN_ORDER)
cf.set(self.SECTION_COLUMN_ORDER, "; Columns will apear in the order they are listed here")
cf.set(self.SECTION_COLUMN_ORDER, "; Titles are case-insensitive")
for i in self.corder:
cf.set(self.SECTION_COLUMN_ORDER, i)
# Write the component grouping fields
cf.add_section(self.SECTION_GROUPING_FIELDS)
cf.set(self.SECTION_GROUPING_FIELDS, '; List of fields used for sorting individual components into groups')
cf.set(self.SECTION_GROUPING_FIELDS, '; Components which match (comparing *all* fields) will be grouped together')
cf.set(self.SECTION_GROUPING_FIELDS, '; Field names are case-insensitive')
for i in self.groups:
cf.set(self.SECTION_GROUPING_FIELDS, i)
cf.add_section(self.SECTION_ALIASES)
cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name")
cf.set(self.SECTION_ALIASES, "; Each line represents a list of equivalent component name values separated by white space")
cf.set(self.SECTION_ALIASES, "; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together")
cf.set(self.SECTION_ALIASES, '; Aliases are case-insensitive')
for a in self.aliases:
cf.set(self.SECTION_ALIASES, "\t".join(a))
cf.add_section(self.SECTION_REGINCLUDES)
cf.set(self.SECTION_REGINCLUDES, '; A series of regular expressions used to include parts in the BoM')
cf.set(self.SECTION_REGINCLUDES, '; If there are any regex defined here, only components that match against ANY of them will be included in the BOM')
cf.set(self.SECTION_REGINCLUDES, '; Column names are case-insensitive')
cf.set(self.SECTION_REGINCLUDES, '; Format is: "[ColumName] [Regex]" (white-space separated)')
for i in self.regIncludes:
if not len(i) == 2:
continue
cf.set(self.SECTION_REGINCLUDES, i[0] + "\t" + i[1])
cf.add_section(self.SECTION_REGEXCLUDES)
cf.set(self.SECTION_REGEXCLUDES, '; A series of regular expressions used to exclude parts from the BoM')
cf.set(self.SECTION_REGEXCLUDES, '; If a component matches ANY of these, it will be excluded from the BoM')
cf.set(self.SECTION_REGEXCLUDES, '; Column names are case-insensitive')
cf.set(self.SECTION_REGEXCLUDES, '; Format is: "[ColumName] [Regex]" (white-space separated)')
for i in self.regExcludes:
if not len(i) == 2:
continue
cf.set(self.SECTION_REGEXCLUDES, i[0] + "\t" + i[1])
with open(file, 'wb') as configfile:
cf.write(configfile)

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
import re
def natural_sort(string):
"""
Natural sorting function which sorts by numerical value of a string,
rather than raw ASCII value.
"""
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string)]

View File

@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
"""
This file contains a set of functions for matching values which may be written in different formats
e.g.
0.1uF = 100n (different suffix specified, one has missing unit)
0R1 = 0.1Ohm (Unit replaces decimal, different units)
"""
from __future__ import unicode_literals
import re
PREFIX_MICRO = [u"μ", "u", "micro"]
PREFIX_MILLI = ["milli", "m"]
PREFIX_NANO = ["nano", "n"]
PREFIX_PICO = ["pico", "p"]
PREFIX_KILO = ["kilo", "k"]
PREFIX_MEGA = ["mega", "meg"]
PREFIX_GIGA = ["giga", "g"]
# All prefixes
PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA
# Common methods of expressing component units
UNIT_R = ["r", "ohms", "ohm", u"Ω"]
UNIT_C = ["farad", "f"]
UNIT_L = ["henry", "h"]
UNIT_ALL = UNIT_R + UNIT_C + UNIT_L
def getUnit(unit):
"""
Return a simplified version of a units string, for comparison purposes
"""
if not unit:
return None
unit = unit.lower()
if unit in UNIT_R:
return "R"
if unit in UNIT_C:
return "F"
if unit in UNIT_L:
return "H"
return None
def getPrefix(prefix):
"""
Return the (numerical) value of a given prefix
"""
if not prefix:
return 1
prefix = prefix.lower()
if prefix in PREFIX_PICO:
return 1.0e-12
if prefix in PREFIX_NANO:
return 1.0e-9
if prefix in PREFIX_MICRO:
return 1.0e-6
if prefix in PREFIX_MILLI:
return 1.0e-3
if prefix in PREFIX_KILO:
return 1.0e3
if prefix in PREFIX_MEGA:
return 1.0e6
if prefix in PREFIX_GIGA:
return 1.0e9
return 1
def groupString(group): # Return a reg-ex string for a list of values
return "|".join(group)
def matchString():
return "^([0-9\.]+)(" + groupString(PREFIX_ALL) + ")*(" + groupString(UNIT_ALL) + ")*(\d*)$"
def compMatch(component):
"""
Return a normalized value and units for a given component value string
e.g. compMatch("10R2") returns (10, R)
e.g. compMatch("3.3mOhm") returns (0.0033, R)
"""
# Remove any commas
component = component.strip().replace(",", "").lower()
match = matchString()
result = re.search(match, component)
if not result:
return None
if not len(result.groups()) == 4:
return None
value, prefix, units, post = result.groups()
# Special case where units is in the middle of the string
# e.g. "0R05" for 0.05Ohm
# In this case, we will NOT have a decimal
# We will also have a trailing number
if post and "." not in value:
try:
value = float(int(value))
postValue = float(int(post)) / (10 ** len(post))
value = value * 1.0 + postValue
except:
return None
try:
val = float(value)
except:
return None
val = "{0:.15f}".format(val * 1.0 * getPrefix(prefix))
return (val, getUnit(units))
def componentValue(valString):
result = compMatch(valString)
if not result:
return valString # Return the same string back
if not len(result) == 2: # Result length is incorrect
return valString
val = result[0]
return val
def compareValues(c1, c2):
""" Compare two values """
r1 = compMatch(c1)
r2 = compMatch(c2)
if not r1 or not r2:
return False
(v1, u1) = r1
(v2, u2) = r2
if v1 == v2:
# Values match
if u1 == u2:
return True # Units match
if not u1:
return True # No units for component 1
if not u2:
return True # No units for component 2
return False

View File

@@ -0,0 +1,2 @@
KIBOM_VERSION = "1.52"
KIBOM_DATE = "2018-9-16"

View File

@@ -0,0 +1,144 @@
# _*_ coding:latin-1 _*_
try:
import xlsxwriter
except:
def WriteXLSX(filename, groups, net, headings, prefs):
return False
else:
import os
"""
Write BoM out to a XLSX file
filename = path to output file (must be a .xlsx file)
groups = [list of ComponentGroup groups]
net = netlist object
headings = [list of headings to display in the BoM file]
prefs = BomPref object
"""
def WriteXLSX(filename, groups, net, headings, prefs):
filename = os.path.abspath(filename)
if not filename.endswith(".xlsx"):
return False
nGroups = len(groups)
nTotal = sum([g.getCount() for g in groups])
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
nBuild = nFitted * prefs.boards
workbook = xlsxwriter.Workbook(filename)
worksheet = workbook.add_worksheet()
if prefs.numberRows:
row_headings = ["Component"] + headings
else:
row_headings = headings
cellformats = {}
column_widths = {}
for i in range(len(row_headings)):
cellformats[i] = workbook.add_format({'align': 'center_across'})
column_widths[i] = len(row_headings[i]) + 10
if not prefs.hideHeaders:
worksheet.write_string(0, i, row_headings[i], cellformats[i])
count = 0
rowCount = 1
for i, group in enumerate(groups):
if prefs.ignoreDNF and not group.isFitted():
continue
row = group.getRow(headings)
if prefs.numberRows:
row = [str(rowCount)] + row
for columnCount in range(len(row)):
cell = row[columnCount].decode('utf-8')
worksheet.write_string(rowCount, columnCount, cell, cellformats[columnCount])
if len(cell) > column_widths[columnCount] - 5:
column_widths[columnCount] = len(cell) + 5
try:
count += group.getCount()
except:
pass
rowCount += 1
if not prefs.hidePcbInfo:
# Add a few blank rows
for i in range(5):
rowCount += 1
cellformat_left = workbook.add_format({'align': 'left'})
worksheet.write_string(rowCount, 0, "Component Groups:", cellformats[0])
worksheet.write_number(rowCount, 1, nGroups, cellformat_left)
rowCount += 1
worksheet.write_string(rowCount, 0, "Component Count:", cellformats[0])
worksheet.write_number(rowCount, 1, nTotal, cellformat_left)
rowCount += 1
worksheet.write_string(rowCount, 0, "Fitted Components:", cellformats[0])
worksheet.write_number(rowCount, 1, nFitted, cellformat_left)
rowCount += 1
worksheet.write_string(rowCount, 0, "Number of PCBs:", cellformats[0])
worksheet.write_number(rowCount, 1, prefs.boards, cellformat_left)
rowCount += 1
worksheet.write_string(rowCount, 0, "Total components:", cellformats[0])
worksheet.write_number(rowCount, 1, nBuild, cellformat_left)
rowCount += 1
worksheet.write_string(rowCount, 0, "Schematic Version:", cellformats[0])
worksheet.write_string(rowCount, 1, net.getVersion(), cellformat_left)
rowCount += 1
if len(net.getVersion()) > column_widths[1]:
column_widths[1] = len(net.getVersion())
worksheet.write_string(rowCount, 0, "Schematic Date:", cellformats[0])
worksheet.write_string(rowCount, 1, net.getSheetDate(), cellformat_left)
rowCount += 1
if len(net.getSheetDate()) > column_widths[1]:
column_widths[1] = len(net.getSheetDate())
worksheet.write_string(rowCount, 0, "BoM Date:", cellformats[0])
worksheet.write_string(rowCount, 1, net.getDate(), cellformat_left)
rowCount += 1
if len(net.getDate()) > column_widths[1]:
column_widths[1] = len(net.getDate())
worksheet.write_string(rowCount, 0, "Schematic Source:", cellformats[0])
worksheet.write_string(rowCount, 1, net.getSource(), cellformat_left)
rowCount += 1
if len(net.getSource()) > column_widths[1]:
column_widths[1] = len(net.getSource())
worksheet.write_string(rowCount, 0, "KiCad Version:", cellformats[0])
worksheet.write_string(rowCount, 1, net.getTool(), cellformat_left)
rowCount += 1
if len(net.getTool()) > column_widths[1]:
column_widths[1] = len(net.getTool())
for i in range(len(column_widths)):
worksheet.set_column(i, i, column_widths[i])
workbook.close()
return True

View File

@@ -0,0 +1,65 @@
"""
Write BoM out to an XML file
filename = path to output file (must be a .xml)
groups = [list of ComponentGroup groups]
net = netlist object
headings = [list of headings to display in the BoM file]
prefs = BomPref object
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from xml.etree import ElementTree
from xml.dom import minidom
def WriteXML(filename, groups, net, headings, prefs):
if not filename.endswith(".xml"):
return False
nGroups = len(groups)
nTotal = sum([g.getCount() for g in groups])
nFitted = sum([g.getCount() for g in groups if g.isFitted()])
nBuild = nFitted * prefs.boards
attrib = {}
attrib['Schematic_Source'] = net.getSource()
attrib['Schematic_Version'] = net.getVersion()
attrib['Schematic_Date'] = net.getSheetDate()
attrib['PCB_Variant'] = ', '.join(prefs.pcbConfig)
attrib['BOM_Date'] = net.getDate()
attrib['KiCad_Version'] = net.getTool()
attrib['Component_Groups'] = str(nGroups)
attrib['Component_Count'] = str(nTotal)
attrib['Fitted_Components'] = str(nFitted)
attrib['Number_of_PCBs'] = str(prefs.boards)
attrib['Total_Components'] = str(nBuild)
xml = ElementTree.Element('KiCad_BOM', attrib=attrib, encoding='utf-8')
for group in groups:
if prefs.ignoreDNF and not group.isFitted():
continue
row = group.getRow(headings)
attrib = {}
for i, h in enumerate(headings):
h = h.replace(' ', '_') # Replace spaces, xml no likey
h = h.replace('"', '')
h = h.replace("'", '')
attrib[h] = str(row[i]).decode('ascii', errors='ignore')
# sub = ElementTree.SubElement(xml, "group", attrib=attrib)
with open(filename, "w") as output:
out = ElementTree.tostring(xml, encoding="utf-8")
output.write(minidom.parseString(out).toprettyxml(indent="\t"))
return True