summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPavel V. Shatov (Meister) <meisterpaul1@yandex.ru>2020-09-23 16:27:10 +0300
committerPavel V. Shatov (Meister) <meisterpaul1@yandex.ru>2020-09-23 16:27:10 +0300
commit78ac59458ac1785ff7872b452149b7f66e770d7e (patch)
tree99fef04b3b41a033f1387a3635fe2b9cc47e1b7f
parentfe7f89177e2dc3bad874ab66ca11979bfe65280e (diff)
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/
-rw-r--r--prepare_production_files.py641
1 files changed, 641 insertions, 0 deletions
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
+# --------------------------------