# # 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' # Assembly Drawing | pcbnew: File -> Print... -> F.Fab, B.Fab (tick "Print mirrored" for the bottom layer!) # # # 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'} _PDF_RENAME_DICT = {'F_Fab' : 'AssemblyTop', 'B_Fab' : 'AssemblyBottom'} _STACKUP = 'Stackup' _NOTES = 'PCB_Notes' _BOM = 'BOM' _GBR = '.gbr' _DRL = '.drl' _POS = '.pos' _XLS = '.xls' _XLSX = _XLS + "x" _TXT = '.txt' _CSV = '.csv' _PDF = '.pdf' _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") print("Step 7. Preparing assembly drawings...") prepare_assembly_drawings() 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 RuntimeE rror # -------------------------------- def prepare_assembly_drawings(): # -------------------------------- # get all the pdfs pdfs = [] for f in os.listdir("%s/%s" % (PROJECT_DIR, _KICAD_GERBER_SUBDIR)): if f.startswith(PROJECT_NAME) and f.endswith(_PDF): pdfs.append(f) # rename files for p in pdfs: d_old_part = p[len(PROJECT_NAME)+1:-len(_PDF)] d_new_part = _PDF_RENAME_DICT[d_old_part] del _PDF_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, _PDF) print(" %s..." % d_from) print(" -> %s" % d_to) shutil.copyfile(d_from, d_to) # check, that everything has been renamed if any(_PDF_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 # --------------------------------