From c56af110070233fd11a6f232fe50f0e010e55a32 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Thu, 3 Sep 2020 11:11:03 -0300 Subject: [PATCH] Added variants to all the PCB plot outputs. Tested for gerbers that are currently excluding pads from *.Paste. --- kibot/misc.py | 4 ++ kibot/out_any_layer.py | 63 ++++++++++++++++++- kibot/out_position.py | 7 +-- tests/board_samples/kibom-variant_3.kicad_pcb | 1 + tests/test_plot/test_gerber.py | 58 +++++++++++++++++ tests/utils/context.py | 6 +- .../yaml_samples/gerber_variant_1.kibot.yaml | 47 ++++++++++++++ 7 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 tests/yaml_samples/gerber_variant_1.kibot.yaml diff --git a/kibot/misc.py b/kibot/misc.py index 571770a2..5e6d39a2 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -48,6 +48,10 @@ AUTO_SCALE = 0 # Internal filter names IFILL_MECHANICAL = '_mechanical' +# KiCad 5 GUI values for the attribute +UI_THT = 0 +UI_SMD = 1 +UI_VIRTUAL = 2 # Supported values for "do not fit" DNF = { diff --git a/kibot/out_any_layer.py b/kibot/out_any_layer.py index 55058747..9da0ea11 100644 --- a/kibot/out_any_layer.py +++ b/kibot/out_any_layer.py @@ -6,13 +6,17 @@ # Project: KiBot (formerly KiPlot) # Adapted from: https://github.com/johnbeard/kiplot import os -from pcbnew import (GERBER_JOBFILE_WRITER, PLOT_CONTROLLER, IsCopperLayer) +from pcbnew import GERBER_JOBFILE_WRITER, PLOT_CONTROLLER, IsCopperLayer, LSET from .out_base import (BaseOutput) from .error import (PlotError, KiPlotConfigurationError) -from .optionable import BaseOptions +from .optionable import BaseOptions, Optionable +from .registrable import RegOutput from .layer import Layer from .gs import GS +from .misc import UI_VIRTUAL +from .kiplot import load_sch from .macros import macros, document # noqa: F401 +from .fil_base import BaseFilter, apply_fitted_filter from . import log logger = log.get_logger(__name__) @@ -38,8 +42,18 @@ class AnyLayerOptions(BaseOptions): """ output file name, the default KiCad name if empty """ self.tent_vias = True """ cover the vias """ + self.variant = '' + """ Board variant(s) to apply """ + self.dnf_filter = Optionable + """ [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill """ super().__init__() + def config(self): + super().config() + self.variant = RegOutput.check_variant(self.variant) + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') + def _configure_plot_ctrl(self, po, output_dir): logger.debug("Configuring plot controller for output") po.SetOutputDirectory(output_dir) @@ -70,6 +84,39 @@ class AnyLayerOptions(BaseOptions): plot_ctrl.SetColorMode(True) + # Apply the variants and filters + exclude = None + if hasattr(self, 'variant') and (self.dnf_filter or self.variant): + load_sch() + # Get the components list from the schematic + comps = GS.sch.get_components() + # Apply the filter + apply_fitted_filter(comps, self.dnf_filter) + # Apply the variant + if self.variant: + # Apply the variant + self.variant.filter(comps) + comps_hash = {c.ref: c for c in comps} + # Remove from solder past layers the filtered components + exclude = LSET() + exclude.addLayer(board.GetLayerID('F.Paste')) + exclude.addLayer(board.GetLayerID('B.Paste')) + old_layers = [] + for m in board.GetModules(): + ref = m.GetReference() + # logger.debug('Ref {}'.format(ref)) + c = comps_hash.get(ref, None) + # logger.debug('Component {}'.format(c)) + if (c and not c.fitted) or m.GetAttributes() == UI_VIRTUAL: + # logger.debug('Removing') + old_c_layers = [] + for p in m.Pads(): + pad_layers = p.GetLayerSet() + old_c_layers.append(pad_layers.FmtHex()) + pad_layers.removeLayerSet(exclude) + p.SetLayerSet(pad_layers) + old_layers.append(old_c_layers) + layers = Layer.solve(layers) # plot every layer in the output for la in layers: @@ -104,6 +151,18 @@ class AnyLayerOptions(BaseOptions): if create_job: jobfile_writer.CreateJobFile(self.expand_filename(output_dir, po.gerber_job_file, 'job', 'gbrjob')) + # Restore the eliminated layers + if exclude: + for m in board.GetModules(): + ref = m.GetReference() + c = comps_hash.get(ref, None) + if (c and not c.fitted) or m.GetAttributes() == UI_VIRTUAL: + restore = old_layers.pop(0) + for p in m.Pads(): + pad_layers = p.GetLayerSet() + res = restore.pop(0) + pad_layers.ParseHex(res, len(res)) + p.SetLayerSet(pad_layers) def read_vals_from_po(self, po): # excludeedgelayer diff --git a/kibot/out_position.py b/kibot/out_position.py index 6e495686..b2b4e069 100644 --- a/kibot/out_position.py +++ b/kibot/out_position.py @@ -11,16 +11,13 @@ from pcbnew import (IU_PER_MM, IU_PER_MILS) from .optionable import BaseOptions, Optionable from .registrable import RegOutput from .gs import GS +from .misc import UI_SMD, UI_VIRTUAL from .kiplot import load_sch from .macros import macros, document, output_class # noqa: F401 from .fil_base import BaseFilter, apply_fitted_filter from . import log logger = log.get_logger(__name__) -# KiCad 5 GUI values for the attribute -UI_THT = 0 -UI_SMD = 1 -UI_VIRTUAL = 2 class PositionOptions(BaseOptions): @@ -167,8 +164,6 @@ class PositionOptions(BaseOptions): # Apply any filter or variant data if comps: c = comps_hash.get(ref, None) - if c: - logger.debug("{} {} {}".format(ref, c.fitted, c.in_bom)) if c and not c.fitted: continue # If passed check the position options diff --git a/tests/board_samples/kibom-variant_3.kicad_pcb b/tests/board_samples/kibom-variant_3.kicad_pcb index 0ab1ba08..09c6d31d 100644 --- a/tests/board_samples/kibom-variant_3.kicad_pcb +++ b/tests/board_samples/kibom-variant_3.kicad_pcb @@ -138,6 +138,7 @@ (fp_line (start -1.68 -0.95) (end 1.68 -0.95) (layer F.CrtYd) (width 0.05)) (fp_line (start 1.68 -0.95) (end 1.68 0.95) (layer F.CrtYd) (width 0.05)) (fp_line (start 1.68 0.95) (end -1.68 0.95) (layer F.CrtYd) (width 0.05)) + (fp_circle (center 0 0) (end 0.4 0) (layer F.Adhes) (width 0.1)) (fp_text user %R (at 0 0) (layer F.Fab) (effects (font (size 0.5 0.5) (thickness 0.08))) ) diff --git a/tests/test_plot/test_gerber.py b/tests/test_plot/test_gerber.py index 55ac74c9..3fa9de13 100644 --- a/tests/test_plot/test_gerber.py +++ b/tests/test_plot/test_gerber.py @@ -19,6 +19,27 @@ from kibot.misc import (PLOT_ERROR) GERBER_DIR = 'gerberdir' +ALL_LAYERS = ['B_Adhes', + 'B_CrtYd', + 'B_Cu', + 'B_Fab', + 'B_Mask', + 'B_Paste', + 'B_SilkS', + 'Cmts_User', + 'Dwgs_User', + 'Eco1_User', + 'Eco2_User', + 'Edge_Cuts', + 'F_Adhes', + 'F_CrtYd', + 'F_Cu', + 'F_Fab', + 'F_Mask', + 'F_Paste', + 'F_SilkS', + 'Margin', + ] def test_gerber_2layer(): @@ -58,3 +79,40 @@ def test_gerber_inner_wrong(): ctx.run(PLOT_ERROR) assert ctx.search_err('is not valid for this board') ctx.clean_up() + + +def compose_fname(dir, prefix, layer, suffix, ext='gbr'): + return os.path.join(dir, prefix+'-'+layer+suffix+'.'+ext) + + +def check_layers_exist(ctx, dir, prefix, layers, suffix): + for layer in layers: + ctx.expect_out_file(compose_fname(dir, prefix, layer, suffix)) + ctx.expect_out_file(compose_fname(dir, prefix, 'job', suffix, 'gbrjob')) + + +def check_components(ctx, dir, prefix, layer, suffix, exclude, include): + fname = compose_fname(dir, prefix, layer, suffix) + inc = [r'%TO\.C,{}\*%'.format(v) for v in include] + ctx.search_in_file(fname, inc) + exc = [r'%TO\.C,{}\*%'.format(v) for v in exclude] + ctx.search_not_in_file(fname, exc) + + +def test_gerber_variant_1(): + prj = 'kibom-variant_3' + ctx = context.TestContext('test_gerber_variant_1', prj, 'gerber_variant_1', GERBER_DIR) + ctx.run() + + # C1 is virtual, not included for all cases + # R3 is a component added to the PCB, included in all cases + # variant: default directory: gerber components: R1, R2 and R3 + check_layers_exist(ctx, 'gerber', prj, ALL_LAYERS, '') + check_components(ctx, 'gerber', prj, 'F_Paste', '', ['C1', 'C2'], ['R1', 'R2', 'R3']) + # variant: production directory: production components: R1, R2, R3 and C2 + check_layers_exist(ctx, 'production', prj, ALL_LAYERS, '_(production)') + check_components(ctx, 'production', prj, 'F_Paste', '_(production)', ['C1'], ['R1', 'R2', 'R3', 'C2']) + # variant: test directory: test components: R1, R3 and C2 + check_layers_exist(ctx, 'test', prj, ALL_LAYERS, '_(test)') + check_components(ctx, 'test', prj, 'F_Paste', '_(test)', ['C1', 'R2'], ['R1', 'R3', 'C2']) + ctx.clean_up() diff --git a/tests/utils/context.py b/tests/utils/context.py index 75adc322..b11b3cff 100644 --- a/tests/utils/context.py +++ b/tests/utils/context.py @@ -304,9 +304,11 @@ class TestContext(object): with open(self.get_out_path(file)) as f: txt = f.read() for t in texts: - logging.debug('- r"'+t+'"') + msg = '- r"'+t+'"' m = re.search(t, txt, re.MULTILINE) - assert m is None + assert m is None, msg + logging.debug(msg+' OK') + # logging.debug(' '+m.group(0)) def compare_image(self, image, reference=None, diff='diff.png', ref_out_dir=False): """ For images and single page PDFs """ diff --git a/tests/yaml_samples/gerber_variant_1.kibot.yaml b/tests/yaml_samples/gerber_variant_1.kibot.yaml new file mode 100644 index 00000000..0af624ce --- /dev/null +++ b/tests/yaml_samples/gerber_variant_1.kibot.yaml @@ -0,0 +1,47 @@ +# Example KiBot config file +kibot: + version: 1 + +variants: + - name: 'production' + comment: 'Production variant' + type: ibom + file_id: '_(production)' + variants_blacklist: T2 + + - name: 'test' + comment: 'Test variant' + type: ibom + file_id: '_(test)' + variants_blacklist: T1 + + - name: 'default' + comment: 'Default variant' + type: ibom + variants_blacklist: T2,T3 + +outputs: + - name: 'gerber' + comment: "Gerber" + type: gerber + dir: gerber + layers: all + options: + variant: default + + - name: 'gerber_production' + comment: "Gerber for production" + type: gerber + dir: production + layers: all + options: + variant: production + + - name: 'gerber_test' + comment: "Gerber for test" + type: gerber + dir: test + layers: all + options: + variant: test +