#!/usr/bin/env kipy __copyright__ = 'Copyright 2020 ALT-TEKNIK LLC' from argparse import ArgumentParser from os import path, rename from os.path import dirname, realpath from pcbnew import B_Cu, B_Mask, Edge_Cuts, EXCELLON_WRITER, F_Cu, F_Fab, \ F_Mask, F_Paste, F_SilkS, FILLED, FromMils, LoadBoard, LSET, \ PCB_PLOT_PARAMS, PLOT_CONTROLLER, PLOT_FORMAT_GERBER, PLOT_FORMAT_PDF, \ TEXTE_PCB from uuid import uuid1 _UNNAMED_PAD = '' class _PadExclusion: def __init__(self, ref, name=None): self.__ref = ref self.__name = (None if name is None else self.__normalize_name(str(name))) def exclude_from_layer(self, board, layer_num): count = 0 layer_mask = ~2**layer_num for pad in board.FindModuleByReference(self.__ref).Pads(): if ((self.__name is None or self.__name == self.__normalize_name(pad.GetName())) and pad.IsOnLayer(layer_num)): pad_mask = int(pad.GetLayerSet().FmtHex().replace('_', ''), 16) pad_hex = hex(pad_mask & layer_mask) layers = LSET() layers.ParseHex(pad_hex, len(pad_hex)) pad.SetLayerSet(layers) count += 1 if count: if self.__name: descr = 'pad {}'.format(self.__name) else: descr = 'pad' if count == 1 else 'pads' if self.__name is not None: descr = 'unnamed {}'.format(descr) print(' Excluded {} {}'.format(self.__ref, descr)) @staticmethod def __normalize_name(name): return _UNNAMED_PAD if name == '~' or name.isspace() else name _BOARD_NAME = 'bgtex' _VERSION_TOKEN = 'X.Y.Z' _GERBER_EXT_OVERRIDES = {F_Fab: 'gm1', Edge_Cuts: 'gko'} class _Layer: def __init__(self, num, descr, **kwargs): self.__num = num self.__descr = descr if kwargs.get('is_pdf'): self.__use_aux_origin = False self.__fmt = PLOT_FORMAT_PDF self.__fmt_descr = 'PDF' else: self.__use_aux_origin = True self.__fmt = PLOT_FORMAT_GERBER self.__fmt_descr = 'Gerber' scale = kwargs.get('scale') self.__scale = 1.0 if not scale else scale suffix = kwargs.get('suffix') self.__suffix = '' if suffix is None else suffix pad_exclusions = kwargs.get('pad_exclusions') self.__pad_exclusions = (() if pad_exclusions is None else pad_exclusions) def plot(self, board_file, output_dir, version=None): # Append the suffix for Gerber layers with file extension overrides to # ease renaming after plotting suffix = self.__suffix override_gerber_ext = (self.__fmt == PLOT_FORMAT_GERBER and self.__num in _GERBER_EXT_OVERRIDES) if override_gerber_ext: uuid = str(uuid1()) suffix = '{}-{}'.format(suffix, uuid) if suffix else uuid # Indicate status print('Plotting {} {}...'.format(self.__descr, self.__fmt_descr)) # Load the board board = LoadBoard(board_file) # Replace version text if version is not None: for drawing in board.GetDrawings(): if (drawing.IsOnLayer(self.__num) and isinstance(drawing, TEXTE_PCB)): text = drawing.GetText() if _VERSION_TOKEN in text: drawing.SetText(text.replace(_VERSION_TOKEN, version)) print(' Replaced version text') # Apply pad exclusions for pad_exclusion in self.__pad_exclusions: pad_exclusion.exclude_from_layer(board, self.__num) # Create a plot controller controller = PLOT_CONTROLLER(board) # Set the plot options opts = controller.GetPlotOptions() opts.SetOutputDirectory(output_dir) opts.SetPlotFrameRef(False) opts.SetPlotValue(False) opts.SetPlotReference(True) opts.SetPlotInvisibleText(False) opts.SetExcludeEdgeLayer(True) opts.SetSubtractMaskFromSilk(False) opts.SetPlotViaOnMaskLayer(False) opts.SetSkipPlotNPTH_Pads(True) opts.SetUseAuxOrigin(self.__use_aux_origin) opts.SetDrillMarksType(PCB_PLOT_PARAMS.NO_DRILL_SHAPE) opts.SetAutoScale(False) opts.SetScale(self.__scale) opts.SetPlotMode(FILLED) opts.SetLineWidth(FromMils(0.1)) opts.SetMirror(False) opts.SetNegative(False) if self.__fmt == PLOT_FORMAT_GERBER: opts.SetUseGerberProtelExtensions(not override_gerber_ext) opts.SetCreateGerberJobFile(False) opts.SetGerberPrecision(6) opts.SetUseGerberX2format(False) opts.SetIncludeGerberNetlistInfo(False) # Plot the layer controller.SetColorMode(False) controller.SetLayer(self.__num) controller.OpenPlotfile(suffix, self.__fmt, self.__descr) controller.PlotLayer() controller.ClosePlot() # Rename Gerber layers with file extension overrides if override_gerber_ext: src = '{}-{}.gbr'.format(_BOARD_NAME, suffix) dest = _BOARD_NAME if self.__suffix: dest = '{}-{}'.format(dest, self.__suffix) dest = '{}.{}'.format(dest, _GERBER_EXT_OVERRIDES[self.__num]) rename(path.join(output_dir, src), path.join(output_dir, dest)) _OUTPUT_DIR = 'plots' _PDF_SCALE = 5.0 _DEFAULT_PASTE_PAD_EXCLUSIONS = ( _PadExclusion('C1'), _PadExclusion('R1'), _PadExclusion('R4'), _PadExclusion('U3', _UNNAMED_PAD) ) _MIN_PASTE_PAD_EXCLUSIONS = ( _PadExclusion('C1'), _PadExclusion('C3'), _PadExclusion('C4'), _PadExclusion('C5'), _PadExclusion('J2'), _PadExclusion('R1'), _PadExclusion('R4'), _PadExclusion('R5'), _PadExclusion('R6'), _PadExclusion('R7'), _PadExclusion('R8'), _PadExclusion('R9'), _PadExclusion('R10'), _PadExclusion('U2'), _PadExclusion('U3') ) _WSON_PASTE_PAD_EXCLUSIONS = ( _PadExclusion('C1'), _PadExclusion('R1'), _PadExclusion('R4') ) _DEFAULT_MASK_PAD_EXCLUSIONS = _PadExclusion('U3', 9), _LAYERS = ( _Layer(F_Fab, 'F.Fab'), _Layer(F_Fab, 'F.Fab', is_pdf=True, scale=_PDF_SCALE, suffix='assembly'), _Layer(F_Paste, 'F.Paste', pad_exclusions=_DEFAULT_PASTE_PAD_EXCLUSIONS), _Layer(F_Paste, 'MIN F.Paste', suffix='min', pad_exclusions=_MIN_PASTE_PAD_EXCLUSIONS), _Layer(F_Paste, 'WSON F.Paste', suffix='wson', pad_exclusions=_WSON_PASTE_PAD_EXCLUSIONS), _Layer(F_SilkS, 'F.SilkS'), _Layer(F_Mask, 'F.Mask', pad_exclusions=_DEFAULT_MASK_PAD_EXCLUSIONS), _Layer(F_Mask, 'WSON F.Mask', suffix='wson'), _Layer(F_Cu, 'F.Cu'), _Layer(B_Cu, 'B.Cu'), _Layer(B_Mask, 'B.Mask'), _Layer(Edge_Cuts, 'Edge.Cuts') ) def main(version=None): # Canonicalize the board filename and output directory project_dir = realpath(dirname(__file__)) board_file = '{}.kicad_pcb'.format(_BOARD_NAME) abs_board_file = path.join(project_dir, board_file) output_dir = realpath(path.join(project_dir, _OUTPUT_DIR)) # Plot the layers for layer in _LAYERS: layer.plot(abs_board_file, output_dir, version) # Write the drill file print('Writing Excellon drill file...') board = LoadBoard(abs_board_file) writer = EXCELLON_WRITER(board) writer.SetOptions(False, # aMirror False, # aMinimalHeader board.GetDesignSettings().m_AuxOrigin, # aOffset True) # aMerge_PTH_NPTH writer.SetRouteModeForOvalHoles(True) writer.SetFormat(True, # aMetric EXCELLON_WRITER.DECIMAL_FORMAT) # aZerosFmt writer.CreateDrillandMapFilesSet(output_dir, # aPlotDirectory True, # aGenDrill False) # aGenMap rename(path.join(output_dir, '{}.drl'.format(_BOARD_NAME)), path.join(output_dir, '{}.xln'.format(_BOARD_NAME))) # Indicate status print('Plotted {}'.format(board_file)) if __name__ == '__main__': parser = ArgumentParser(description='creates BGTEX board plots') parser.add_argument('version', nargs='?', help='the board version') args = parser.parse_args() main(args.version)