#
# Simple script to make KiCAD-generated output files more usable
#
#
# Imports
#
import os
import shutil
from enum import Enum, auto
from bisect import bisect
from openpyxl import Workbook
#
# Stuff we need to handle:
#
# Gerber | pcbnew: File -> Plot...
# NC Drill | pcbnew: File -> Fabrication Outputs -> Drill File...
# BOM | eeschema: Tools -> Generate Bill of Materials... (install 'bom_csv_sorted_by_ref_extra' first!)
# Stackup | 'Stackup.xls'
# Pick & Place Data | pcbnew: File -> Fabrication Outputs -> Footprint Position File...
# Notes | 'PCB_Notes.txt'
#
#
# Settings
#
# name and location of KiCAD project
PROJECT_NAME = 'Cryptech Alpha'
PROJECT_DIR = './KiCAD'
# where to store production files bundle
TARGET_DIR = './ProductionFiles_rev04'
TARGET_PREFIX = 'Alpha_rev04'
#
# Internals
#
_KICAD_GERBER_SUBDIR = 'GerberOutput'
_OUT_GERBER_DIR = "%s/Gerbers" % TARGET_DIR
_GERBER_RENAME_DICT = {'F_Cu' : 'TopCopper',
'F_Mask' : 'TopMask',
'F_Paste' : 'TopPaste',
'F_SilkS' : 'TopSilk',
'In1_Cu' : 'InternalCopper1',
'In2_Cu' : 'InternalCopper2',
'In3_Cu' : 'InternalCopper3',
'In4_Cu' : 'InternalCopper4',
'In5_Cu' : 'InternalCopper5',
'In6_Cu' : 'InternalCopper6',
'B_Cu' : 'BottomCopper',
'B_Mask' : 'BottomMask',
'B_Paste' : 'BottomPaste',
'B_SilkS' : 'BottomSilk',
'Edge_Cuts' : 'Border'}
_DRILL_RENAME_DICT = {'PTH' : 'Plated',
'NPTH' : 'NonPlated'}
_POS_RENAME_DICT = {'top' : 'TopPickPlace',
'bottom' : 'BottomPickPlace'}
_STACKUP = 'Stackup'
_NOTES = 'PCB_Notes'
_BOM = 'BOM'
_GBR = '.gbr'
_DRL = '.drl'
_POS = '.pos'
_XLS = '.xls'
_XLSX = _XLS + "x"
_TXT = '.txt'
_CSV = '.csv'
_SKIP_REFDES = ['LOGO1']
_BOM_REPLACE_CASE = {'Cryptech_Alpha_Footprints:R_0402' : '0402',
'Resistor_SMD:R_0805_2012Metric' : '0805',
'Cryptech_Alpha_Footprints:C_0402' : '0402',
'Cryptech_Alpha_Footprints:C_0805' : '0805',
'Cryptech_Alpha_Footprints:C_0603' : '0603',
'Cryptech_Alpha_Footprints:C_1210' : '1210',
'Cryptech_Alpha_Footprints:C_D' : 'D'}
_EXCEL_COL_A = 'A'
_BOM_COLUMNS = ['RefDes', 'PartNumber', 'Manufacturer', 'Value', 'Package', 'Tolerance', 'Voltage', 'Quantity', 'Comment']
class BOM_Resistor():
def __init__(self, csv_line):
csv_line_parts = csv_line.strip()[1:-1].split('","')
# "Ref","Value","Footprint","Datasheet","Manufacturer","Vendor","Tolerance","Voltage","Dielectric","Comment"
self.refdes = csv_line_parts[0]
self.value = csv_line_parts[1]
self.footprint = _BOM_REPLACE_CASE[csv_line_parts[2]]
#datasheet [3]
#manufacturer [4]
#vendor [5]
self.tolerance = csv_line_parts[6]
#voltage[7]
#dielectric[8]
#comment[9]
class BOM_Capacitor():
def __init__(self, csv_line):
csv_line_parts = csv_line.strip()[1:-1].split('","')
# "Ref","Value","Footprint","Datasheet","Manufacturer","Vendor","Tolerance","Voltage","Dielectric","Comment"
self.refdes = csv_line_parts[0]
self.value = csv_line_parts[1]
self.footprint = _BOM_REPLACE_CASE[csv_line_parts[2]]
#datasheet [3]
#manufacturer [4]
#vendor [5]
self.tolerance = csv_line_parts[6]
self.voltage = csv_line_parts[7]
self.dielectric = csv_line_parts[8]
#comment[9]
class BOM_Generic():
def __init__(self, csv_line):
csv_line_parts = csv_line.strip()[1:-1].split('","')
# "Ref","Value","Footprint","Datasheet","Manufacturer","Vendor","Tolerance","Voltage","Dielectric","Comment"
self.refdes = csv_line_parts[0]
self.partnumber = csv_line_parts[1]
self.footprint = csv_line_parts[2]
#datasheet [3]
self.manufacturer = csv_line_parts[4]
#vendor [5]
#tolerance [6]
#voltage[7]
#dielectric[8]
self.comment = csv_line_parts[9]
class BOM_ResistorGroup():
def __init__(self, value, footprint, tolerance):
self.refdes = []
self.value = value
self.footprint = footprint
self.tolerance = tolerance
def __contains__(self, r):
if r.value != self.value: return False
if r.footprint != self.footprint: return False
if r.tolerance != self.tolerance: return False
return True
def add(self, r):
assert r.value == self.value
assert r.footprint == self.footprint
assert r.tolerance == self.tolerance
self.refdes.append(r.refdes)
class BOM_CapacitorGroup():
def __init__(self, value, footprint, tolerance, voltage, dielectric):
self.refdes = []
self.value = value
self.footprint = footprint
self.tolerance = tolerance
self.voltage = voltage
self.dielectric = dielectric
def __contains__(self, c):
if c.value != self.value: return False
if c.footprint != self.footprint: return False
if c.tolerance != self.tolerance: return False
if c.voltage != self.voltage: return False
if c.dielectric != self.dielectric: return False
return True
def add(self, c):
assert c.value == self.value
assert c.footprint == self.footprint
assert c.tolerance == self.tolerance
assert c.voltage == self.voltage
assert c.dielectric == self.dielectric
self.refdes.append(c.refdes)
class BOM_GenericGroup():
def __init__(self, partnumber, manufacturer, comment):
self.refdes = []
self.partnumber = partnumber
self.manufacturer = manufacturer
self.comment = comment
def __contains__(self, g):
if g.partnumber != self.partnumber: return False
if g.manufacturer != self.manufacturer: return False
if g.comment != self.comment: return False
return True
def add(self, g):
assert g.partnumber == self.partnumber
assert g.manufacturer == self.manufacturer
assert g.comment == self.comment
self.refdes.append(g.refdes)
class BOM_EntryType(Enum):
RESISTOR = auto()
CAPACITOR = auto()
GENERIC = auto()
class BOM_Entry():
def _fill(self, refdes,
partnumber="?", manufacturer="?",
value="?", footprint="?", tolerance="?", voltage="?", dielectric="?",
comment="?",
type=BOM_EntryType.GENERIC):
self.refdes = ', '.join(refdes)
self.quantity = len(refdes)
self.partnumber, self.manufacturer = (
partnumber, manufacturer)
self.value, self.footprint, self.tolerance, self.voltage, self.dielectric = (
value, footprint, tolerance, voltage, dielectric)
self.comment = comment
self.type = type
def fill_from_resistor(self, r):
self._fill(r.refdes,
value=r.value, footprint=r.footprint, tolerance=r.tolerance,
type=BOM_EntryType.RESISTOR)
def fill_from_capacitor(self, c):
self._fill(c.refdes,
value=c.value, footprint=c.footprint, tolerance=c.tolerance, voltage=c.voltage, dielectric=c.dielectric,
type=BOM_EntryType.CAPACITOR)
def fill_from_generic(self, g):
self._fill(g.refdes,
partnumber=g.partnumber, manufacturer=g.manufacturer,
comment=g.comment,
type=BOM_EntryType.GENERIC)
def format(self):
#
# RefDes, PartNumber, Manufacturer, Value, Package, Tolerance, Voltage, Quantity, Comment]
#
if self.type == BOM_EntryType.RESISTOR:
return [self.refdes, '', '', self.value, self.footprint, self.tolerance, '', self.quantity, '']
elif self.type == BOM_EntryType.CAPACITOR:
return [self.refdes, '', '', self.value, self.footprint, self.tolerance, self.voltage, self.quantity, self.dielectric]
elif self.type == BOM_EntryType.GENERIC:
return [self.refdes, self.partnumber, self.manufacturer, '', '', '', '', self.quantity, self.comment]
else:
raise RuntimeError
# --------------------------------
def main():
# --------------------------------
print("Step 0. Creating dirs...")
create_dirs()
print(" Done")
print("Step 1. Preparing Gerbers...")
prepare_gerbers()
print(" Done")
print("Step 2. Preparing drills...")
prepare_drills()
print(" Done")
print("Step 3. Preparing BOM...")
prepare_bom()
print(" Done")
print("Step 4. Preparing stackup...")
prepare_stackup()
print(" Done")
print("Step 5. Preparing pick&place data...")
prepare_pickplace()
print(" Done")
print("Step 6. Preparing notes...")
prepare_notes()
print(" Done")
# --------------------------------
def create_dirs():
# --------------------------------
_create_dirs_stub(TARGET_DIR)
_create_dirs_stub(_OUT_GERBER_DIR)
# --------------------------------
def _create_dirs_stub(d):
# --------------------------------
print(" %s" % d)
os.makedirs(d, exist_ok=True)
# --------------------------------
def prepare_gerbers():
# --------------------------------
# get all the gerbers
gbrs = []
for f in os.listdir("%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR)):
if f.startswith(PROJECT_NAME) and f.endswith(_GBR):
gbrs.append(f)
# rename files
for g in gbrs:
g_old_part = g[len(PROJECT_NAME)+1:-len(_GBR)]
g_new_part = _GERBER_RENAME_DICT[g_old_part]
del _GERBER_RENAME_DICT[g_old_part]
g_from = "%s/%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR, g)
g_to = "%s/%s_%s%s" % (_OUT_GERBER_DIR, TARGET_PREFIX, g_new_part, _GBR)
print(" %s..." % g_from)
print(" -> %s" % g_to)
shutil.copyfile(g_from, g_to)
# check, that everything has been renamed
if any(_GERBER_RENAME_DICT):
raise RuntimeError
# --------------------------------
def prepare_drills():
# --------------------------------
# get all the drills
drls = []
for f in os.listdir("%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR)):
if f.startswith(PROJECT_NAME) and f.endswith(_DRL):
drls.append(f)
# rename files
for d in drls:
d_old_part = d[len(PROJECT_NAME)+1:-len(_DRL)]
d_new_part = _DRILL_RENAME_DICT[d_old_part]
del _DRILL_RENAME_DICT[d_old_part]
d_from = "%s/%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR, d)
d_to = "%s/%s_%s%s" % (_OUT_GERBER_DIR, TARGET_PREFIX, d_new_part, _DRL)
print(" %s..." % d_from)
print(" -> %s" % d_to)
shutil.copyfile(d_from, d_to)
# check, that everything has been renamed
if any(_DRILL_RENAME_DICT):
raise RuntimeError
# --------------------------------
def prepare_bom():
# --------------------------------
# load bom (for some reason eeschema doesn't add '.csv' to output file -- FIXED)
with open("%s/%s" % (PROJECT_DIR, (PROJECT_NAME + _CSV))) as f:
csvs = f.readlines()
# strip header lines
csvs = csvs[5:]
BOM_G = [] # everything else
BOM_C = [] # capacitors
BOM_R = [] # resistors
for i in range(len(csvs)):
csvi = csvs[i]
# logo, test points
skip = False
for s in _SKIP_REFDES:
if csvi.startswith('"'+s):
skip = True
break
if skip: continue
if csvi.startswith('"R'):
BOM_R.append(BOM_Resistor(csvi))
continue
if csvi.startswith('"C') and not csvi.startswith('"CN'):
BOM_C.append(BOM_Capacitor(csvi))
continue
g = BOM_Generic(csvi)
# validate
if g.partnumber == "": raise RuntimeError
if g.manufacturer == "": raise RuntimeError(g.refdes)
BOM_G.append(g)
BOM_GRP_G = _bom_group_generic(BOM_G)
BOM_GRP_R = _bom_group_resistors(BOM_R)
BOM_GRP_C = _bom_group_capacitors(BOM_C)
# re-enable should you want to compare against rev.03
if False:
_dump_bom_resistors(BOM_GRP_R)
if False:
_dump_bom_capacitors(BOM_GRP_C)
# validate resistos
for r in BOM_GRP_R:
if r.tolerance == "": raise RuntimeError
# validate capacitors
for c in BOM_GRP_C:
if c.tolerance == "": raise RuntimeError
if c.voltage == "": raise RuntimeError
if c.dielectric == "": raise RuntimeError
# glue everything together
BOM = []
for grp_r in BOM_GRP_R:
r = BOM_Entry()
r.fill_from_resistor(grp_r)
BOM.append(r)
for grp_c in BOM_GRP_C:
c = BOM_Entry()
c.fill_from_capacitor(grp_c)
BOM.append(c)
for grp_g in BOM_GRP_G:
g = BOM_Entry()
g.fill_from_generic(grp_g)
BOM.append(g)
BOM_SORTED = _sort_bom(BOM)
_write_bom_xlsx(BOM_SORTED)
def _sort_bom(BOM):
BOM_SORTED = []
REFDES = []
BOM_SORTED.append(BOM[0])
REFDES.append(BOM[0].refdes)
for i in range(1, len(BOM)):
BOMi = BOM[i]
j = bisect(REFDES, BOMi.refdes)
BOM_SORTED.insert(j, BOMi)
REFDES.insert(j, BOMi.refdes)
return BOM_SORTED
def _write_bom_xlsx(BOM):
# make up target path
bom_path = "%s/%s_%s%s" % (TARGET_DIR, TARGET_PREFIX, _BOM, _XLSX)
print(" %s" % bom_path)
# generate Excel workbook
wb = Workbook()
ws = wb.active
# fill header
for i in range(len(_BOM_COLUMNS)):
bci = _BOM_COLUMNS[i]
idx = "%s1" % chr(ord(_EXCEL_COL_A)+i)
ws[idx] = bci
# body
for i in range(len(BOM)):
BOMi = BOM[i]
fmt = BOMi.format()
for j in range(len(fmt)):
fmtj = fmt[j]
idx = "%s%d" % (chr(ord(_EXCEL_COL_A)+j), (i+2))
ws[idx] = fmtj
# save
wb.save(bom_path)
# --------------------------------
def prepare_stackup():
# --------------------------------
s_from = "%s/%s%s" % ('.', _STACKUP, _XLS)
s_to = "%s/%s_%s%s" % (TARGET_DIR, TARGET_PREFIX, _STACKUP, _XLS)
print(" %s..." % s_from)
print(" -> %s" % s_to)
shutil.copyfile(s_from, s_to)
# --------------------------------
def prepare_pickplace():
# --------------------------------
# get all the positions
poss = []
for f in os.listdir("%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR)):
if f.startswith(PROJECT_NAME) and f.endswith(_POS):
poss.append(f)
# rename files
for p in poss:
d_old_part = p[len(PROJECT_NAME)+1:-len(_POS)]
d_new_part = _POS_RENAME_DICT[d_old_part]
del _POS_RENAME_DICT[d_old_part]
d_from = "%s/%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR, p)
d_to = "%s/%s_%s%s" % (_OUT_GERBER_DIR, TARGET_PREFIX, d_new_part, _POS)
print(" %s..." % d_from)
print(" -> %s" % d_to)
shutil.copyfile(d_from, d_to)
# check, that everything has been renamed
if any(_POS_RENAME_DICT):
raise RuntimeError
# --------------------------------
def prepare_notes():
# --------------------------------
n_from = "%s/%s%s" % ('.', _NOTES, _TXT)
n_to = "%s/%s_%s%s" % (TARGET_DIR, TARGET_PREFIX, _NOTES, _TXT)
print(" %s..." % n_from)
print(" -> %s" % n_to)
shutil.copyfile(n_from, n_to)
# --------------------------------
def _bom_group_resistors(R):
# --------------------------------
G = []
for r in R:
# check if group already exists
mapped = False
for g in G:
if r in g:
g.add(r)
mapped = True
break
# shortcut
if mapped: continue
# new group
G.append(BOM_ResistorGroup(r.value, r.footprint, r.tolerance))
G[-1].add(r)
return G
# --------------------------------
def _bom_group_capacitors(C):
# --------------------------------
G = []
for c in C:
# check if group already exists
mapped = False
for g in G:
if c in g:
g.add(c)
mapped = True
break
# shortcut
if mapped: continue
# new group
G.append(BOM_CapacitorGroup(c.value, c.footprint, c.tolerance, c.voltage, c.dielectric))
G[-1].add(c)
return G
# --------------------------------
def _bom_group_generic(G):
# --------------------------------
GG = []
for g in G:
# check if group already exists
mapped = False
for gg in GG:
if g in gg:
gg.add(g)
mapped = True
break
# shortcut
if mapped: continue
# new group
GG.append(BOM_GenericGroup(g.partnumber, g.manufacturer, g.comment))
GG[-1].add(g)
return GG
# --------------------------------
def _dump_bom_resistors(GR):
# --------------------------------
for g in GR:
refdes = ', '.join(g.refdes)
print("%s %s %s %s %d" % (refdes, g.value, g.tolerance, g.footprint, len(g.refdes)))
# --------------------------------
def _dump_bom_capacitors(GC):
# --------------------------------
for g in GC:
refdes = ', '.join(g.refdes)
print("%s %s %s %s %s %s %d" % (refdes, g.value, g.voltage, g.tolerance, g.footprint, g.dielectric, len(g.refdes)))
# --------------------------------
if __name__ == '__main__':
# --------------------------------
main()
# --------------------------------
# End-of-File
# --------------------------------