kicad/scripts/KiBoM/bomlib/component.py
2020-02-04 17:33:02 -07:00

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