* Added ability to group components by any arbitrary fields (set in bom.ini file)
* User can determine whether blank fields are merged (or not)
* Improved part grouping
* User can set a 'Configuration' field to determine whether a part is loaded or not under certain circumstances
* Cleanup of preferences management
* Added version number
This commit is contained in:
Oliver 2016-07-04 21:50:54 +10:00
parent 75c46504d9
commit 0afa7bb26e
4 changed files with 197 additions and 120 deletions

View File

@ -35,14 +35,6 @@ class Component():
#no match, return False
return False
#compare footprint with another component
def compareFootprint(self, other):
return self.getFootprint().lower() == other.getFootprint().lower()
#compare the component library of this part to another part
def compareLibName(self, other):
return self.getLibName().lower() == other.getLibName().lower()
#determine if two parts have the same name
def comparePartName(self, other):
pn1 = self.getPartName().lower()
@ -57,24 +49,43 @@ class Component():
return True
return False
def compareField(self, other, field):
this_field = self.getField(field).lower()
other_field = other.getField(field).lower()
if this_field == other_field: return True
#if blank comparisons are allowed
if self.prefs.mergeBlankFields:
if this_field == "" or other_field == "":
return True
return False
#Equivalency operator is used to determine if two parts are 'equal'
def __eq__(self, other):
"""Equlivalency operator, remember this can be easily overloaded"""
valueResult = self.compareValue(other)
#if connector comparison is overridden, set valueResult to True
if self.prefs.groupConnectors:
if "conn" in self.getDescription().lower():
valueResult = True
if self.prefs.compareFootprints:
fpResult = self.compareFootprint(other)
else:
fpResult = True
results = []
#'fitted' value must be the same for both parts
results.append(self.isFitted() == other.isFitted())
for c in self.prefs.groups:
#perform special matches
if c.lower() == ColumnList.COL_VALUE.lower():
results.append(self.compareValue(other))
#match part name
elif c.lower() == ColumnList.COL_PART.lower():
results.append(self.comparePartName(other))
return valueResult and fpResult and self.compareLibName(other) and self.comparePartName(other) and self.isFitted() == other.isFitted()
#generic match
else:
results.append(self.compareField(other, c))
return all(results)
def setLibPart(self, part):
self.libpart = part
@ -107,7 +118,7 @@ class Component():
def getValue(self):
return self.element.get("value")
def getField(self, name, libraryToo=True):
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
@ -118,10 +129,40 @@ class Component():
libraryToo -- look in the libpart's fields for the same name if not found
in component itself
"""
#special fields
if name.lower() == ColumnList.COL_REFERENCE.lower():
return self.getRef()
if name.lower() == ColumnList.COL_DESCRIPTION.lower():
return self.getDescription()
if name.lower() == ColumnList.COL_DATASHEET.lower():
return self.getDatasheet()
if name.lower() == ColumnList.COL_FP.lower():
return self.getFootprint().split(":")[0]
if name.lower() == ColumnList.COL_FP.lower():
return self.getFootprint().split(":")[1]
if name.lower() == ColumnList.COL_VALUE.lower():
return self.getValue()
if name.lower() == ColumnList.COL_PART.lower():
return self.getPartName()
if name.lower() == ColumnList.COL_PART_LIB.lower():
return self.getLibName()
field = self.element.get("field", "name", name)
if field == "" and libraryToo:
field = self.libpart.getField(name)
return field
def getFieldNames(self):
@ -142,13 +183,31 @@ class Component():
#determine if a component is FITTED or not
def isFitted(self):
check = [self.getValue().lower(), self.getField("Notes").lower()]
for item in check:
if any([dnf in item for dnf in DNF]): return False
return True
check = self.getField(self.prefs.configField).lower()
#check the value field first
if self.getValue().lower() in DNF or check.lower() in DNF:
return False
if check == "":
return True #empty is fitted
opts = check.split(",")
result = True
for opt in opts:
#options that start with '-' are explicitly removed from certain configurations
if opt.startswith('-') and opt[1:].lower() == self.prefs.pcbConfig.lower():
result = False
break
if opt.startswith("+"):
if opt.lower() == self.prefs.pcbConfig.lower():
result = True
#by default, part is fitted
return result
def getFootprint(self, libraryToo=True):
ret = self.element.get("footprint")
@ -180,6 +239,7 @@ class ComponentGroup():
self.prefs = prefs
def getField(self, field):
if not field in self.fields.keys(): return ""
if not self.fields[field]: return ""
return u''.join((self.fields[field]))
@ -191,6 +251,8 @@ class ComponentGroup():
def matchComponent(self, c):
if len(self.components) == 0: return True
if c == self.components[0]: return True
return False
#test if a given component is already contained in this grop
def containsComponent(self, c):
@ -260,6 +322,7 @@ class ComponentGroup():
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()
@ -278,7 +341,11 @@ class ComponentGroup():
#return True if none match (i.e. this group is OK)
#retunr False if any match
def testRegex(self):
return True
#run the excusion
"""
for key in self.prefs.regex.keys():
reg = self.prefs.regex[key]
if not type(reg) in [str, list]: continue #regex must be a string, or a list of strings
@ -303,6 +370,7 @@ class ComponentGroup():
return False
return True
"""
#return a dict of the KiCAD data based on the supplied columns

View File

@ -12,46 +12,57 @@ class BomPref:
SECTION_IGNORE = "IGNORE_COLUMNS"
SECTION_GENERAL = "BOM_OPTIONS"
SECTION_EXCLUDE_VALUES = "EXCLUDE_COMPONENT_VALUES"
SECTION_EXCLUDE_REFS = "EXCLUDE_COMPONENT_REFS"
SECTION_EXCLUDE_FP = "EXCLUDE_COMPONENT_FP"
SECTION_EXCLUDE_PART = "EXCLUDE_COMPONENT_PART"
SECTION_EXCLUDE_DESC = "EXCLUDE_COMPONENT_DESC"
SECTION_ALIASES = "COMPONENT_ALIASES"
SECTION_CONFIGURATIONS = "PCB_CONFIGURATIONS"
SECTION_GROUPING_FIELDS = "GROUP_FIELDS"
SECTION_REGEXCLUDES = "REGEX_EXCLUDE"
SECTION_REGINCLUDES = "REGEX_INCLUDE"
OPT_IGNORE_DNF = "ignore_dnf"
OPT_PCB_CONFIG = "pcb_configuration"
OPT_NUMBER_ROWS = "number_rows"
OPT_GROUP_CONN = "group_connectors"
OPT_USE_REGEX = "test_regex"
OPT_COMP_FP = "compare_footprints"
OPT_INC_PRICE = "calculate_price"
#list of columns which we can use regex on
COL_REG_EX = [
ColumnList.COL_REFERENCE,
ColumnList.COL_DESCRIPTION,
ColumnList.COL_VALUE,
ColumnList.COL_FP,
ColumnList.COL_FP_LIB,
ColumnList.COL_PART,
ColumnList.COL_PART_LIB
]
OPT_MERGE_BLANK = "merge_blank_fields"
OPT_IGNORE_DNF = "ignore_dnf"
OPT_CONFIG_FIELD = "configuration_field"
def __init__(self):
self.ignore = [
ColumnList.COL_PART_LIB,
ColumnList.COL_FP_LIB,
] #list of headings to ignore in BoM generation
self.ignoreDNF = False #ignore rows for do-not-fit parts
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.compareFootprints = True #test footprints when comparing components
self.boards = 1
self.mergeBlankFields = True #blanks fields will be merged when possible
self.hideHeaders = False
self.verbose = False #by default, is not verbose
self.configurations = [] #list of various configurations
self.configField = "Config" #default field used for part fitting config
self.pcbConfig = "default"
#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_PART,'mount[\s-_]*hole'],
[ColumnList.COL_PART,'solder[\s-_]*bridge'],
[ColumnList.COL_PART,'test[\s-_]*point'],
[ColumnList.COL_FP,'test[\s-_]*point'],
[ColumnList.COL_FP,'mount[\s-_]*hole'],
[ColumnList.COL_FP,'fiducial'],
]
#default component groupings
self.aliases = [
@ -62,29 +73,6 @@ class BomPref:
["zener","zenersmall"],
["d","diode","d_small"]
]
#dictionary of possible regex expressions for ignoring component row(s)
self.regex = dict.fromkeys(self.COL_REG_EX)
#default regex values
self.regex[ColumnList.COL_REFERENCE] = [
'TP[0-9]+',
]
self.regex[ColumnList.COL_PART] = [
'mounthole',
'scopetest',
'mount_hole',
'solder_bridge',
'test_point',
]
self.regex[ColumnList.COL_FP] = [
'mounthole'
]
def columnToGroup(self, col):
return "REGEXCLUDE_" + col.upper().replace(" ","_")
#check an option within the SECTION_GENERAL group
def checkOption(self, parser, opt, default=False):
@ -102,6 +90,7 @@ class BomPref:
with open(file, 'rb') as configfile:
cf = ConfigParser.RawConfigParser(allow_no_value = True)
cf.optionxform=str
cf.read(file)
@ -111,12 +100,18 @@ class BomPref:
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.compareFootprints = self.checkOption(cf, self.OPT_COMP_FP, default=True)
#read out configurations
if self.SECTION_CONFIGURATIONS in cf.sections():
self.configurations = [i for i in cf.options(self.SECTION_CONFIGURATIONS)]
self.mergeBlankFields = self.checkOption(cf, self.OPT_MERGE_BLANK, default = True)
if cf.has_option(self.SECTION_GENERAL, self.OPT_PCB_CONFIG):
self.pcbConfig = cf.get(self.SECTION_GENERAL, self.OPT_PCB_CONFIG)
if cf.has_option(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD):
self.configField = cf.get(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD)
#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)]
@ -125,12 +120,11 @@ class BomPref:
if self.SECTION_ALIASES in cf.sections():
self.aliases = [a.split(" ") for a in cf.options(self.SECTION_ALIASES)]
#read out the regex
for key in self.regex.keys():
section = self.columnToGroup(key)
if section in cf.sections():
self.regex[key] = [r for r in cf.options(section)]
if self.SECTION_REGEXCLUDES in cf.sections():
pass
if self.SECTION_REGINCLUDES in cf.sections():
pass
#add an option to the SECTION_GENRAL group
def addOption(self, parser, opt, value, comment=None):
@ -145,6 +139,7 @@ class BomPref:
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")
@ -152,7 +147,14 @@ class BomPref:
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_COMP_FP, self.compareFootprints, comment="If '{opt}' option is set to 1, two components must have the same footprint to be grouped together. If '{opt}' is not set, then footprint comparison is ignored.".format(opt=self.OPT_COMP_FP))
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, '; 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, "; Configuration string used to determine which components are loaded on a particular board")
cf.set(self.SECTION_GENERAL, '; Configuration string is case-insensitive')
cf.set(self.SECTION_GENERAL, self.OPT_PCB_CONFIG, self.pcbConfig)
cf.add_section(self.SECTION_IGNORE)
cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM")
@ -161,36 +163,40 @@ class BomPref:
for i in self.ignore:
cf.set(self.SECTION_IGNORE, i)
cf.add_section(self.SECTION_CONFIGURATIONS)
cf.set(self.SECTION_CONFIGURATIONS, '; List of PCB configuration parameters')
for i in self.configurations:
cf.set(self.SECTION_CONFIGURATIONS, 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-SENSITIVE!')
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 space-separated list of equivalent component name values")
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, " ".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, '; Column names are case-insensitive')
for i in self.regIncludes:
if not len(i) == 2: continue
for col in self.regex.keys():
cf.set(self.SECTION_REGINCLUDE, i[0], i[1])
reg = self.regex[col]
section = self.columnToGroup(col)
cf.add_section(section)
#comments
cf.set(section, "; A list of regex to compare against the '{col}' column".format(col=col))
cf.set(section, "; If the value in the '{col}' column matches any of these expressions, the row will be excluded from the BoM".format(col=col))
if type(reg) == str:
cf.set(section, reg)
elif type(reg) == list:
for r in reg:
cf.set(section, r)
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_REGINCLUDES, '; Column names are case-insensitive')
for i in self.regExcludes:
if not len(i) == 2: continue
cf.set(self.SECTION_REGEXCLUDES, i[0], i[1])
with open(file, 'wb') as configfile:
cf.write(configfile)

1
KiBOM/version.py Normal file
View File

@ -0,0 +1 @@
KIBOM_VERSION = 1.2

View File

@ -32,9 +32,9 @@ parser = argparse.ArgumentParser(description="KiBOM Bill of Materials generator
parser.add_argument("netlist", help='xml netlist file. Use "%%I" when running from within KiCad')
parser.add_argument("output", default="", help='BoM output file name.\nUse "%%O" when running from within KiCad to use the default output name (csv file).\nFor e.g. HTML output, use "%%O.html"')
parser.add_argument("-b", "--boards", help="Number of boards to build (default = 1)", type=int, default=1)
parser.add_argument("-n", "--number", help="Number of boards to build (default = 1)", type=int, default=1)
parser.add_argument("-v", "--verbose", help="Enable verbose output", action='count')
parser.add_argument("-n", "--noheader", help="Do not generate file headers; data only.", action='count')
parser.add_argument("-r", "--revision", help="Board variant, used to determine which components are output to the BoM", type=str, default=None)
parser.add_argument("--cfg", help="BoM config file (script will try to use 'bom.ini' if not specified here)")
args = parser.parse_args()
@ -62,20 +62,22 @@ if args.cfg:
#read preferences from file. If file does not exists, default preferences will be used
pref = BomPref()
#pass various command-line options through
pref.verbose = verbose
pref.boards = args.boards
if args.noheader:
pref.hideHeaders = True
if os.path.exists(config_file):
pref.Read(config_file)
say("Config:",config_file)
#pass various command-line options through
pref.verbose = verbose
pref.boards = args.number
if args.revision is not None:
pref.pcbConfig = args.revision
print("PCB Revision:",args.revision)
#write preference file back out (first run will generate a file with default preferences)
if not os.path.exists(ini):
pref.Write(ini)
say("Writing preferences file bom.ini")
pref.Write(ini)
say("Writing preferences file bom.ini")
#individual components
components = []