move everything to 'old' directory before merge
This commit is contained in:
0
old/scripts/KiBoM/bomlib/__init__.py
Normal file
0
old/scripts/KiBoM/bomlib/__init__.py
Normal file
86
old/scripts/KiBoM/bomlib/bom_writer.py
Normal file
86
old/scripts/KiBoM/bomlib/bom_writer.py
Normal 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
|
126
old/scripts/KiBoM/bomlib/columns.py
Normal file
126
old/scripts/KiBoM/bomlib/columns.py
Normal 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)
|
576
old/scripts/KiBoM/bomlib/component.py
Normal file
576
old/scripts/KiBoM/bomlib/component.py
Normal 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
|
92
old/scripts/KiBoM/bomlib/csv_writer.py
Normal file
92
old/scripts/KiBoM/bomlib/csv_writer.py
Normal 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
|
139
old/scripts/KiBoM/bomlib/html_writer.py
Normal file
139
old/scripts/KiBoM/bomlib/html_writer.py
Normal 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
|
484
old/scripts/KiBoM/bomlib/netlist_reader.py
Normal file
484
old/scripts/KiBoM/bomlib/netlist_reader.py
Normal 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()
|
299
old/scripts/KiBoM/bomlib/preferences.py
Normal file
299
old/scripts/KiBoM/bomlib/preferences.py
Normal 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)
|
11
old/scripts/KiBoM/bomlib/sort.py
Normal file
11
old/scripts/KiBoM/bomlib/sort.py
Normal 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)]
|
171
old/scripts/KiBoM/bomlib/units.py
Normal file
171
old/scripts/KiBoM/bomlib/units.py
Normal 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
|
2
old/scripts/KiBoM/bomlib/version.py
Normal file
2
old/scripts/KiBoM/bomlib/version.py
Normal file
@@ -0,0 +1,2 @@
|
||||
KIBOM_VERSION = "1.52"
|
||||
KIBOM_DATE = "2018-9-16"
|
144
old/scripts/KiBoM/bomlib/xlsx_writer.py
Normal file
144
old/scripts/KiBoM/bomlib/xlsx_writer.py
Normal 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
|
65
old/scripts/KiBoM/bomlib/xml_writer.py
Normal file
65
old/scripts/KiBoM/bomlib/xml_writer.py
Normal 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
|
Reference in New Issue
Block a user