From 95edf0b1fe48c0f879330097496bc06ed9140ec4 Mon Sep 17 00:00:00 2001 From: "Nathan L. Conrad" Date: Fri, 7 Aug 2020 18:24:46 -0500 Subject: [PATCH] Add plot and release scripts --- README | 30 ++++++++- plot.py | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ release | 105 ++++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+), 2 deletions(-) create mode 100755 plot.py create mode 100755 release diff --git a/README b/README index 79534ad..b8360d2 100644 --- a/README +++ b/README @@ -2,6 +2,32 @@ Adapter to widen the row spacing of a 2x10 0.1" through-hole connector to 0.3" -Copyright 2020 ALT-TEKNIK LLC -Created with KiCad 5.1.6 (macOS) +DEPENDENCIES + +- KiCad version 5.1.6 for macOS. Results with previous or subsequent versions + will likely vary. +- kipy (https://git.alt-tek.com/alt-tek/kipy) is useful to simplify executing + scripts with KiCad's Python environment. It is required to generate board + plots with plot.py for releases. +- tea (https://gitea.com/gitea/tea) is required to generate releases. Install + and write your API token to ~/.config/git.alt-tek.com/api-token. Restrict + access to your API token by setting its file permissions accordingly + (i.e. 'chmod 600 ~/.config/git.alt-tek.com/api-token'). +- In order to generate releases, SSH must be configured for git.alt-tek.com + such that SSH Git commands can be executed without explicitly specifying an + SSH identity or private key + + +RELEASING + +1. See above dependencies +2. Create an annotated Git tag for the release commit using + "git tag -a v -m 'Version ' " where version is the + board version in X.Y.Z form and is the commit hash + (e.g. "git tag -a v1.2.3 -m 'Version 1.2.3' d15c0cafe") +3. Push the tag upstream (i.e. 'git push origin tags/v') +4. Use the release script to build the release and upload it as a draft + (i.e. './release ') +5. Review the release draft. If it is ready for pre-release or production, + publish it. diff --git a/plot.py b/plot.py new file mode 100755 index 0000000..cee8ead --- /dev/null +++ b/plot.py @@ -0,0 +1,193 @@ +#!/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 = '2x10-dip-adapter' +_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' +_LAYERS = ( + _Layer(F_Mask, 'F.Mask'), + _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 board plots') + parser.add_argument('version', nargs='?', help='the board version') + args = parser.parse_args() + main(args.version) diff --git a/release b/release new file mode 100755 index 0000000..902c1b7 --- /dev/null +++ b/release @@ -0,0 +1,105 @@ +#!/usr/bin/env bash -e +# +# Copyright 2020 ALT-TEKNIK LLC + +# Parse arguments +gitea_domain=git.alt-tek.com +unset version +while [[ $# -gt 0 ]] +do + arg=$1 + shift + case $arg in + -h|--help) + IFS= read -rd '' usage << __eof__ || true +Usage: release [-h] version + +Builds a release and uploads it to $gitea_domain as a draft + +Positional arguments: + version The release version + +Optional arguments: + -h, --help Show this help message and exit +__eof__ + echo -n "$usage" + exit 0 + ;; + -*|--*) + echo "Invalid argument '$arg'. Use -h for help." >&2 + exit 1 + ;; + *) + if [[ -v version ]] + then + echo "Invalid argument '$arg'. Use -h for help." >&2 + exit 1 + fi + version=$arg + esac +done +if [[ -z $version ]] +then + echo 'Missing version argument. Use -h for help.' >&2 + exit 1 +fi + +# Initialize globals used by handle-trap +signals='INT TERM' +unset tmp_dir +prj_name=2x10-dip-adapter +tea_login=$prj_name-release + +# Usage: handle-trap [exit_code] +# +# Deletes temporary files and logs out of Tea upon script exit or signal +# +# Positional arguments: +# exit_code Optional exit code for which to trigger a script exit +function handle-trap { + trap - EXIT $signals + rm -rf "$tmp_dir" + tea logout -n "$tea_login" + if [[ $1 ]] + then + exit "$1" + fi +} + +# Set the trap +trap handle-trap EXIT +trap 'handle-trap 1' $signals + +# Create a temporary build directory +tmp_dir=$(mktemp -d) + +# Git a clean clone +repo=alt-tek/$prj_name +git_dir=$tmp_dir/git +git clone "$gitea_domain:$repo" "$git_dir" +cd "$git_dir" +tag=v$version +git checkout "tags/$tag" -b "tmp/$tag-build" + +# Create the archive directory +archive_name=${prj_name}_$version +archive_dir=$tmp_dir/$archive_name +mkdir "$archive_dir" + +# Generate board plots +plot_dir=$git_dir/plots +rm -rf "$plot_dir" +./plot.py "$version" +cp "$plot_dir"/* "$archive_dir" + +# Tar and Gzip the archive +archive=$tmp_dir/$archive_name.tar.gz +tar -czf "$archive" -C "$tmp_dir" "$archive_name" + +# Draft the release +tea_token=$(<"$HOME/.config/$gitea_domain/api-token") +tea login add -n "$tea_login" -u "https://$gitea_domain" -t "$tea_token" +title="Version $version" +tea releases create -l "$tea_login" -r "$repo" --tag "$tag" -d -t "$title" \ + -a "$archive" +echo "Drafted $title at https://$gitea_domain/$repo/releases"