From 78ac59458ac1785ff7872b452149b7f66e770d7e Mon Sep 17 00:00:00 2001 From: "Pavel V. Shatov (Meister)" Date: Wed, 23 Sep 2020 16:27:10 +0300 Subject: Use this script to generate a bundle of files required for board production. First, use pcbnew to plot Gerbers and NC Drill, then export BOM (install the custom plugin from helper/ first) and generate footprint position files. When run the script will automatically assemble everything in ./ProductionFiles_rev04/ --- prepare_production_files.py | 641 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 prepare_production_files.py diff --git a/prepare_production_files.py b/prepare_production_files.py new file mode 100644 index 0000000..757164c --- /dev/null +++ b/prepare_production_files.py @@ -0,0 +1,641 @@ +# +# 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 +# -------------------------------- -- cgit v1.2.3