summaryrefslogblamecommitdiff
path: root/prepare_production_files.py
blob: df5abb8936c64925dedfe4eeb46db0eb7e7f795d (plain) (tree)























                                                                                                                     
                                                                                                            











































                                                       



                                               










                      
              

























































































































































































































                                                                                                                                  



                                                   








































































































































































































































                                                                                    

                      

                                  

























                                                                                










































































































                                                                                                                           
#
# 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
# --------------------------------