577 lines
17 KiB
Python
577 lines
17 KiB
Python
# -*- 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
|