diff --git a/kiplot/__main__.py b/kiplot/__main__.py index cc8042aa..522fc1e2 100644 --- a/kiplot/__main__.py +++ b/kiplot/__main__.py @@ -11,49 +11,42 @@ import os import sys import gzip from glob import glob +from logging import DEBUG # Import log first to set the domain from . import log log.set_domain('kiplot') -from . import kiplot -from . import config_reader -from . import misc +from .kiplot import (GS, generate_outputs) +from .pre_base import (BasePreFlight) +from .config_reader import (CfgYamlReader) +from .misc import (NO_PCB_FILE, EXIT_BAD_ARGS) from .__version__ import __version__ def main(): - - parser = argparse.ArgumentParser( - description='Command-line Plotting for KiCad') - parser.add_argument('target', nargs='*', - help='Outputs to generate, default is all') + parser = argparse.ArgumentParser(description='Command-line Plotting for KiCad') + parser.add_argument('target', nargs='*', help='Outputs to generate, default is all') group = parser.add_mutually_exclusive_group() - parser.add_argument('-b', '--board-file', - help='The PCB .kicad-pcb board file') - parser.add_argument('-c', '--plot-config', - help='The plotting config file to use') - parser.add_argument('-d', '--out-dir', default='.', - help='The output directory (cwd if not given)') - parser.add_argument('-i', '--invert-sel', action='store_true', - help='Generate the outputs not listed as targets') - parser.add_argument('-l', '--list', action='store_true', - help='List available outputs') - group.add_argument('-q', '--quiet', action='store_true', - help='remove information logs') - parser.add_argument('-s', '--skip-pre', nargs=1, - help='skip pre-flight actions, comma separated list ' - 'or `all`') - group.add_argument('-v', '--verbose', action='store_true', - help='show debugging information') - parser.add_argument('--version', '-V', action='version', - version='%(prog)s '+__version__+' - ' + + parser.add_argument('-b', '--board-file', help='The PCB .kicad-pcb board file') + parser.add_argument('-c', '--plot-config', help='The plotting config file to use') + parser.add_argument('-d', '--out-dir', default='.', help='The output directory (cwd if not given)') + parser.add_argument('-i', '--invert-sel', action='store_true', help='Generate the outputs not listed as targets') + parser.add_argument('-l', '--list', action='store_true', help='List available outputs') + group.add_argument('-q', '--quiet', action='store_true', help='remove information logs') + parser.add_argument('-s', '--skip-pre', nargs=1, help='skip pre-flight actions, comma separated list or `all`') + group.add_argument('-v', '--verbose', action='store_true', help='show debugging information') + parser.add_argument('--version', '-V', action='version', version='%(prog)s '+__version__+' - ' + __copyright__+' - License: '+__license__) - args = parser.parse_args() # Create a logger with the specified verbosity logger = log.init(args.verbose, args.quiet) + GS.debug_enabled = logger.getEffectiveLevel() <= DEBUG + # Output dir: relative to CWD (absolute path overrides) + GS.out_dir = os.path.join(os.getcwd(), args.out_dir) + + # Determine the PCB file if args.board_file is None: board_files = glob('*.kicad_pcb') if len(board_files) == 1: @@ -65,13 +58,19 @@ def main(): ' Using '+board_file+' if you want to use another use -b option.') else: logger.error('No PCB file found (*.kicad_pcb), use -b to specify one.') - sys.exit(misc.EXIT_BAD_ARGS) + sys.exit(EXIT_BAD_ARGS) else: board_file = args.board_file if not os.path.isfile(board_file): logger.error("Board file not found: "+board_file) - sys.exit(misc.NO_PCB_FILE) + sys.exit(NO_PCB_FILE) + GS.pcb_file = board_file + GS.sch_file = os.path.splitext(board_file)[0] + '.sch' + if not os.path.isfile(GS.sch_file): + logger.warning('Missing schematic file: ' + GS.sch_file) + GS.sch_file = None + # Determine the YAML file if args.plot_config is None: plot_configs = glob('*.kiplot.yaml') if len(plot_configs) == 1: @@ -83,48 +82,37 @@ def main(): ' Using '+plot_config+' if you want to use another use -c option.') else: logger.error('No config file found (*.kiplot.yaml), use -c to specify one.') - sys.exit(misc.EXIT_BAD_ARGS) + sys.exit(EXIT_BAD_ARGS) else: plot_config = args.plot_config if not os.path.isfile(plot_config): logger.error("Plot config file not found: "+plot_config) - sys.exit(misc.EXIT_BAD_ARGS) - - cr = config_reader.CfgYamlReader(board_file) + sys.exit(EXIT_BAD_ARGS) + # Read the config file + cr = CfgYamlReader(board_file) + outputs = None try: # The Python way ... with gzip.open(plot_config) as cf_file: - cfg = cr.read(cf_file) + outputs = cr.read(cf_file) except OSError: + pass + if outputs is None: with open(plot_config) as cf_file: - cfg = cr.read(cf_file) - - # relative to CWD (absolute path overrides) - outdir = os.path.join(os.getcwd(), args.out_dir) - cfg.outdir = outdir - - # Finally, once all value are in, check they make sense - errs = cfg.validate() - - if errs: - logger.error('Invalid config:\n' + "\n".join(errs)) - sys.exit(misc.EXIT_BAD_CONFIG) + outputs = cr.read(cf_file) + # Actions if args.list: - logger.info('\npre-flight:\n' - 'run_erc: '+str(cfg.run_erc)+'\n' - 'run_drc: '+str(cfg.run_drc)+'\n' - 'update_xml: '+str(cfg.update_xml)+'\n') + logger.info('\nPre-flight:') + pf = BasePreFlight.get_in_use_objs() + for c in pf: + logger.info('- '+str(c)) logger.info('Outputs:') - for op in cfg.outputs: - logger.info('%s (%s) [%s]' % (op.name, op.description, - op.options.type)) - sys.exit(0) - - # Set up the plotter and do it - plotter = kiplot.Plotter(cfg) - plotter.plot(board_file, args.target, args.invert_sel, args.skip_pre) + for o in outputs: + logger.info('- '+str(o)) + else: + generate_outputs(outputs, args.target, args.invert_sel, args.skip_pre) if __name__ == "__main__": diff --git a/kiplot/config_reader.py b/kiplot/config_reader.py index 0e9e7172..2335a1a4 100644 --- a/kiplot/config_reader.py +++ b/kiplot/config_reader.py @@ -7,9 +7,35 @@ import sys import pcbnew -from . import plot_config as PC +from .error import (KiPlotConfigurationError) +from .kiplot import (Layer, get_layer_id_from_pcb) +from .misc import (NO_YAML_MODULE, EXIT_BAD_CONFIG) +# Output classes +from .out_base import BaseOutput +from . import out_gerber # noqa: F401 +from . import out_ps # noqa: F401 +from . import out_hpgl # noqa: F401 +from . import out_dxf # noqa: F401 +from . import out_pdf # noqa: F401 +from . import out_svg # noqa: F401 +from . import out_gerb_drill # noqa: F401 +from . import out_excellon # noqa: F401 +from . import out_position # noqa: F401 +from . import out_step # noqa: F401 +from . import out_kibom # noqa: F401 +from . import out_ibom # noqa: F401 +from . import out_pdf_sch_print # noqa: F401 +from . import out_pdf_pcb_print # noqa: F401 +# PreFlight classes +from .pre_base import BasePreFlight +from . import pre_drc # noqa: F401 +from . import pre_erc # noqa: F401 +from . import pre_update_xml # noqa: F401 +from . import pre_check_zone_fills # noqa: F401 +from . import pre_ignore_unconnected # noqa: F401 +from . import pre_filters # noqa: F401 +# Logger from . import log -from . import misc logger = log.get_logger(__name__) @@ -18,431 +44,33 @@ try: except ImportError: # pragma: no cover log.init(False, False) logger.error('No yaml module for Python, install python3-yaml') - sys.exit(misc.NO_YAML_MODULE) - - -# note - type IDs are strings form the _config_, not the internal -# strings used as enums (in plot_config) -ANY_LAYER = ['gerber', 'ps', 'svg', 'hpgl', 'pdf', 'dxf'] -ANY_DRILL = ['excellon', 'gerb_drill'] - - -class CfgReader(object): - def __init__(self): - pass + sys.exit(NO_YAML_MODULE) def config_error(msg): logger.error(msg) - sys.exit(misc.EXIT_BAD_CONFIG) + sys.exit(EXIT_BAD_CONFIG) -def load_layers(kicad_pcb_file): - layer_names = ['-']*50 - pcb_file = open(kicad_pcb_file, "r") - collect_layers = False - for line in pcb_file: - if collect_layers: - z = re.match(r'\s+\((\d+)\s+(\S+)', line) - if z: - res = z.groups() - # print(res[1]+'->'+res[0]) - layer_names[int(res[0])] = res[1] - else: - if re.search(r'^\s+\)$', line): - collect_layers = False - break - else: - if re.search(r'\s+\(layers', line): - collect_layers = True - pcb_file.close() - return layer_names - - -class CfgYamlReader(CfgReader): - +class CfgYamlReader(object): def __init__(self, brd_file): super(CfgYamlReader, self).__init__() - self.layer_names = load_layers(brd_file) - - def _check_version(self, data): - - try: - version = data['kiplot']['version'] - except (KeyError, TypeError): - config_error("YAML config needs kiplot.version.") + def _check_version(self, v): + if not isinstance(v, dict): + config_error("Incorrect `kiplot` section") + if 'version' not in v: + config_error("YAML config needs `kiplot.version`.") + version = v['version'] + # Only version 1 is known if version != 1: config_error("Unknown KiPlot config version: "+str(version)) - return version - def _get_required(self, data, key, context=None): - - try: - val = data[key] - except (KeyError, TypeError): - config_error("Missing `"+key+"' "+('' - if context is None else context)) - - return val - - def _parse_drill_map(self, map_opts): - - mo = PC.DrillMapOptions() - - TYPES = { - 'hpgl': pcbnew.PLOT_FORMAT_HPGL, - 'ps': pcbnew.PLOT_FORMAT_POST, - 'gerber': pcbnew.PLOT_FORMAT_GERBER, - 'dxf': pcbnew.PLOT_FORMAT_DXF, - 'svg': pcbnew.PLOT_FORMAT_SVG, - 'pdf': pcbnew.PLOT_FORMAT_PDF - } - - type_s = self._get_required(map_opts, 'type', 'in drill map section') - - try: - mo.type = TYPES[type_s] - except KeyError: - config_error("Unknown drill map type: "+type_s) - - return mo - - def _parse_drill_report(self, report_opts): - - opts = PC.DrillReportOptions() - - opts.filename = self._get_required(report_opts, 'filename', - 'in drill report section') - - return opts - - def _perform_config_mapping(self, otype, cfg_options, mapping_list, - target, name): - """ - Map a config dict onto a target object given a mapping list - """ - - for mapping in mapping_list: - - # if this output type matches the mapping specification: - if otype in mapping['types']: - - key = mapping['key'] - - # set the internal option as needed - if mapping['required'](cfg_options): - - cfg_val = self._get_required(cfg_options, key, 'in ' + - otype + ' section') - elif not(cfg_options is None) and key in cfg_options: - # not required but given anyway - cfg_val = cfg_options[key] - else: - continue - - # transform the value if needed - if 'transform' in mapping: - cfg_val = mapping['transform'](cfg_val) - - try: - setattr(target, mapping['to'], cfg_val) - except PC.KiPlotConfigurationError as e: - config_error("In section '"+name+"' ("+otype+"): "+str(e)) - - def _parse_out_opts(self, otype, options, name): - - # mappings from YAML keys to type_option keys - MAPPINGS = [ - { - 'key': 'use_aux_axis_as_origin', - 'types': ['gerber', 'dxf'], - 'to': 'use_aux_axis_as_origin', - 'required': lambda opts: True, - }, - { - 'key': 'exclude_edge_layer', - 'types': ANY_LAYER, - 'to': 'exclude_edge_layer', - 'required': lambda opts: True, - }, - { - 'key': 'exclude_pads_from_silkscreen', - 'types': ANY_LAYER, - 'to': 'exclude_pads_from_silkscreen', - 'required': lambda opts: True, - }, - { - 'key': 'plot_sheet_reference', - 'types': ANY_LAYER, - 'to': 'plot_sheet_reference', - 'required': lambda opts: True, - }, - { - 'key': 'plot_footprint_refs', - 'types': ANY_LAYER, - 'to': 'plot_footprint_refs', - 'required': lambda opts: True, - }, - { - 'key': 'plot_footprint_values', - 'types': ANY_LAYER, - 'to': 'plot_footprint_values', - 'required': lambda opts: True, - }, - { - 'key': 'force_plot_invisible_refs_vals', - 'types': ANY_LAYER, - 'to': 'force_plot_invisible_refs_vals', - 'required': lambda opts: True, - }, - { - 'key': 'tent_vias', - 'types': ANY_LAYER, - 'to': 'tent_vias', - 'required': lambda opts: True, - }, - { - 'key': 'check_zone_fills', - 'types': ANY_LAYER, - 'to': 'check_zone_fills', - 'required': lambda opts: True, - }, - { - 'key': 'line_width', - 'types': ['gerber', 'ps', 'svg', 'pdf'], - 'to': 'line_width', - 'required': lambda opts: True, - }, - { - 'key': 'subtract_mask_from_silk', - 'types': ['gerber'], - 'to': 'subtract_mask_from_silk', - 'required': lambda opts: True, - }, - { - 'key': 'mirror_plot', - 'types': ['ps', 'svg', 'hpgl', 'pdf'], - 'to': 'mirror_plot', - 'required': lambda opts: True, - }, - { - 'key': 'negative_plot', - 'types': ['ps', 'svg', 'pdf'], - 'to': 'negative_plot', - 'required': lambda opts: True, - }, - { - 'key': 'sketch_plot', - 'types': ['ps', 'hpgl'], - 'to': 'sketch_plot', - 'required': lambda opts: True, - }, - { - 'key': 'scaling', - 'types': ['ps', 'hpgl'], - 'to': 'scaling', - 'required': lambda opts: True, - }, - { - 'key': 'drill_marks', - 'types': ['ps', 'svg', 'dxf', 'hpgl', 'pdf'], - 'to': 'drill_marks', - 'required': lambda opts: True, - }, - { - 'key': 'use_protel_extensions', - 'types': ['gerber'], - 'to': 'use_protel_extensions', - 'required': lambda opts: True, - }, - { - 'key': 'gerber_precision', - 'types': ['gerber'], - 'to': 'gerber_precision', - 'required': lambda opts: True, - }, - { - 'key': 'create_gerber_job_file', - 'types': ['gerber'], - 'to': 'create_gerber_job_file', - 'required': lambda opts: True, - }, - { - 'key': 'use_gerber_x2_attributes', - 'types': ['gerber'], - 'to': 'use_gerber_x2_attributes', - 'required': lambda opts: True, - }, - { - 'key': 'use_gerber_net_attributes', - 'types': ['gerber'], - 'to': 'use_gerber_net_attributes', - 'required': lambda opts: True, - }, - { - 'key': 'scale_adjust_x', - 'types': ['ps'], - 'to': 'scale_adjust_x', - 'required': lambda opts: True, - }, - { - 'key': 'scale_adjust_y', - 'types': ['ps'], - 'to': 'scale_adjust_y', - 'required': lambda opts: True, - }, - { - 'key': 'width_adjust', - 'types': ['ps'], - 'to': 'width_adjust', - 'required': lambda opts: True, - }, - { - 'key': 'a4_output', - 'types': ['ps'], - 'to': 'a4_output', - 'required': lambda opts: True, - }, - { - 'key': 'pen_width', - 'types': ['hpgl'], - 'to': 'pen_width', - 'required': lambda opts: True, - }, - { - 'key': 'polygon_mode', - 'types': ['dxf'], - 'to': 'polygon_mode', - 'required': lambda opts: True, - }, - { - 'key': 'use_aux_axis_as_origin', - 'types': ANY_DRILL, - 'to': 'use_aux_axis_as_origin', - 'required': lambda opts: True, - }, - { - 'key': 'map', - 'types': ANY_DRILL, - 'to': 'map_options', - 'required': lambda opts: False, - 'transform': self._parse_drill_map - }, - { - 'key': 'report', - 'types': ANY_DRILL, - 'to': 'report_options', - 'required': lambda opts: False, - 'transform': self._parse_drill_report - }, - { - 'key': 'metric_units', - 'types': ['excellon', 'step'], - 'to': 'metric_units', - 'required': lambda opts: True, - }, - { - 'key': 'pth_and_npth_single_file', - 'types': ['excellon'], - 'to': 'pth_and_npth_single_file', - 'required': lambda opts: True, - }, - { - 'key': 'minimal_header', - 'types': ['excellon'], - 'to': 'minimal_header', - 'required': lambda opts: True, - }, - { - 'key': 'mirror_y_axis', - 'types': ['excellon'], - 'to': 'mirror_y_axis', - 'required': lambda opts: True, - }, - { - 'key': 'format', - 'types': ['position', 'kibom'], - 'to': 'format', - 'required': lambda opts: True, - }, - { - 'key': 'units', - 'types': ['position'], - 'to': 'units', - 'required': lambda opts: True, - }, - { - 'key': 'separate_files_for_front_and_back', - 'types': ['position'], - 'to': 'separate_files_for_front_and_back', - 'required': lambda opts: True, - }, - { - 'key': 'only_smd', - 'types': ['position'], - 'to': 'only_smd', - 'required': lambda opts: True, - }, - { - 'key': 'blacklist', - 'types': ['ibom'], - 'to': 'blacklist', - 'required': lambda opts: False, - }, - { - 'key': 'name_format', - 'types': ['ibom'], - 'to': 'name_format', - 'required': lambda opts: False, - }, - { - 'key': 'output', - 'types': ['pdf_sch_print'], - 'to': 'output', - 'required': lambda opts: False, - }, - { - 'key': 'output_name', - 'types': ['pdf_pcb_print'], - 'to': 'output_name', - 'required': lambda opts: True, - }, - { - 'key': 'origin', - 'types': ['step'], - 'to': 'origin', - 'required': lambda opts: True, - }, - { - 'key': 'no_virtual', - 'types': ['step'], - 'to': 'no_virtual', - 'required': lambda opts: False, - }, - { - 'key': 'min_distance', - 'types': ['step'], - 'to': 'min_distance', - 'required': lambda opts: False, - }, - ] - - po = PC.OutputOptions(otype) - - # options that apply to the specific output type - to = po.type_options - - self._perform_config_mapping(otype, options, MAPPINGS, to, name) - - return po - def _get_layer_from_str(self, s): """ Get the pcbnew layer from a string in the config """ - D = { 'F.Cu': pcbnew.F_Cu, 'B.Cu': pcbnew.B_Cu, @@ -465,97 +93,112 @@ class CfgYamlReader(CfgReader): 'F.Fab': pcbnew.F_Fab, 'B.Fab': pcbnew.B_Fab, } - layer = None - # Priority # 1) Internal list if s in D: - layer = PC.LayerInfo(D[s], False, s) + layer = Layer(D[s], False, s) else: - try: + id = get_layer_id_from_pcb(s) + if id is not None: # 2) List from the PCB - id = self.layer_names.index(s) - layer = PC.LayerInfo(id, id < pcbnew.B_Cu, s) - except ValueError: + layer = Layer(id, id < pcbnew.B_Cu, s) + elif s.startswith("Inner"): # 3) Inner.N names - if s.startswith("Inner"): - m = re.match(r"^Inner\.([0-9]+)$", s) - if not m: - config_error('Malformed inner layer name: ' + - s + ', use Inner.N') + m = re.match(r"^Inner\.([0-9]+)$", s) + if not m: + raise KiPlotConfigurationError("Malformed inner layer name: {}, use Inner.N".format(s)) + layer = Layer(int(m.group(1)), True, s) + else: + raise KiPlotConfigurationError('Unknown layer name: '+s) + return layer - layer = PC.LayerInfo(int(m.group(1)), True, s) + def _parse_layers(self, layers_to_parse): + # Check we have a list of layers + if not layers_to_parse: + raise KiPlotConfigurationError("Missing `layers` definition") + if not isinstance(layers_to_parse, list): + raise KiPlotConfigurationError("`layers` must be a list") + # Parse the elements + layers = [] + for l in layers_to_parse: + # Check they are dictionaries + if not isinstance(l, dict): + raise KiPlotConfigurationError("Malformed `layer` entry ({})".format(l)) + # Extract the attributes + layer = None + description = 'no desc' + suffix = '' + for k, v in l.items(): + if k == 'layer': + layer = v + elif k == 'description': + description = v + elif k == 'suffix': + suffix = v else: - config_error('Unknown layer name: '+s) - return layer - - def _parse_layer(self, l_obj, context): - - l_str = self._get_required(l_obj, 'layer', context) - layer_id = self._get_layer_from_str(l_str) - layer = PC.LayerConfig(layer_id) - - layer.desc = l_obj['description'] if 'description' in l_obj else None - layer.suffix = l_obj['suffix'] if 'suffix' in l_obj else "" - - return layer + raise KiPlotConfigurationError("Unknown {} attribute for `layer`".format(v)) + # Check we got the layer name + if layer is None: + raise KiPlotConfigurationError("Missing `layer` attribute for layer entry ({})".format(l)) + # Create an object for it + layer = self._get_layer_from_str(layer) + layer.set_extra(suffix, description) + layers.append(layer) + return layers def _parse_output(self, o_obj): - - try: - name = o_obj['name'] - except KeyError: + # Default values + name = None + desc = None + otype = None + options = None + outdir = '.' + layers = [] + # Parse all of them + for k, v in o_obj.items(): + if k == 'name': + name = v + elif k == 'comment': + desc = v + elif k == 'type': + otype = v + elif k == 'options': + options = v + elif k == 'dir': + outdir = v + elif k == 'layers': + layers = v + else: + config_error("Unknown key `{}` in `{}` ({})".format(k, name, otype)) + # Validate them + if not name: config_error("Output needs a name in: "+str(o_obj)) + if not otype: + config_error("Output `"+name+"` needs a type") + name_type = "`"+name+"` ("+otype+")" + # Is a valid type? + if not BaseOutput.is_registered(otype): + config_error("Unknown output type: `{}`".format(otype)) + # Load it + logger.debug("Parsing output options for "+name_type) + o_out = BaseOutput.get_class_for(otype)(name, otype, desc) + # Apply the options try: - desc = o_obj['comment'] - except KeyError: - desc = None + # If we have layers parse them + if layers: + layers = self._parse_layers(layers) + o_out.config(outdir, options, layers) + except KiPlotConfigurationError as e: + config_error("In section '"+name+"' ("+otype+"): "+str(e)) - try: - otype = o_obj['type'] - except KeyError: - config_error("Output '"+name+"' needs a type") + return o_out - if otype not in ['gerber', 'ps', 'hpgl', 'dxf', 'pdf', 'svg', - 'gerb_drill', 'excellon', 'position', 'step', - 'kibom', 'ibom', 'pdf_sch_print', 'pdf_pcb_print']: - config_error("Unknown output type '"+otype+"' in '"+name+"'") - - try: - options = o_obj['options'] - except KeyError: - if otype not in ['ibom', 'pdf_sch_print']: - config_error("Output '"+name+"' needs options") - options = None - - logger.debug("Parsing output options for {} ({})".format(name, otype)) - - outdir = self._get_required(o_obj, 'dir', 'in section `' + name + - '` ('+otype+')') - - output_opts = self._parse_out_opts(otype, options, name) - - o_cfg = PC.PlotOutput(name, desc, otype, output_opts) - o_cfg.outdir = outdir - - try: - layers = o_obj['layers'] - except KeyError: - if otype == 'pdf_pcb_print' or otype in ANY_LAYER: - logger.error('You must specify the layers for `' + name + - '` ('+otype+')') - sys.exit(misc.EXIT_BAD_CONFIG) - layers = [] - - for l in layers: - o_cfg.layers.append(self._parse_layer(l, 'in section ' + name + - ' ('+otype+')')) - - return o_cfg - - def _parse_filters(self, filters, cfg): + def _parse_filters(self, filters): + if not isinstance(filters, list): + config_error("'filters' must be a list") + parsed = None for filter in filters: if 'filter' in filter: comment = filter['filter'] @@ -571,31 +214,32 @@ class CfgYamlReader(CfgReader): config_error("empty 'regex' in 'filter' definition ("+str(filter)+")") else: config_error("missing 'regex' for 'filter' definition ("+str(filter)+")") - cfg.add_filter(comment, number, regex) + logger.debug("Adding DRC/ERC filter '{}','{}','{}'".format(comment, number, regex)) + if parsed is None: + parsed = '' + if not comment: + parsed += '# '+comment+'\n' + parsed += '{},{}\n'.format(number, regex) else: config_error("'filters' section of 'preflight' must contain 'filter' definitions (not "+str(filter)+")") + return parsed - def _parse_preflight(self, pf, cfg): - + def _parse_preflight(self, pf): logger.debug("Parsing preflight options: {}".format(pf)) + if not isinstance(pf, dict): + config_error("Incorrect `preflight` section") - if 'check_zone_fills' in pf: - cfg.check_zone_fills = pf['check_zone_fills'] - - if 'run_drc' in pf: - cfg.run_drc = pf['run_drc'] - - if 'run_erc' in pf: - cfg.run_erc = pf['run_erc'] - - if 'update_xml' in pf: - cfg.update_xml = pf['update_xml'] - - if 'ignore_unconnected' in pf: - cfg.ignore_unconnected = pf['ignore_unconnected'] - - if 'filters' in pf: - self._parse_filters(pf['filters'], cfg) + for k, v in pf.items(): + if not BasePreFlight.is_registered(k): + config_error("Unknown preflight: `{}`".format(k)) + try: + logger.debug("Parsing preflight "+k) + if k == 'filters': + v = self._parse_filters(v) + o_pre = BasePreFlight.get_class_for(k)(k, v) + except KiPlotConfigurationError as e: + config_error("In preflight '"+k+"': "+str(e)) + BasePreFlight.add_preflight(o_pre) def read(self, fstream): """ @@ -603,24 +247,28 @@ class CfgYamlReader(CfgReader): :param fstream: file stream of a config YAML file """ - try: data = yaml.load(fstream) - except yaml.YAMLError: - config_error("Error loading YAML") - - self._check_version(data) - - cfg = PC.PlotConfig() - - if 'preflight' in data: - self._parse_preflight(data['preflight'], cfg) - - try: - for o in data['outputs']: - op_cfg = self._parse_output(o) - cfg.add_output(op_cfg) - except KeyError: - pass - - return cfg + except yaml.YAMLError as e: + config_error("Error loading YAML "+str(e)) + # List of outputs + outputs = [] + version = None + # Analize each section + for k, v in data.items(): + # logger.debug('{} {}'.format(k, v)) + if k == 'kiplot': + version = self._check_version(v) + elif k == 'preflight': + self._parse_preflight(v) + elif k == 'outputs': + if isinstance(v, list): + for o in v: + outputs.append(self._parse_output(o)) + else: + config_error("`outputs` must be a list") + else: + logger.warning('Skipping unknown `{}` section in config.'.format(k)) + if version is None: + config_error("YAML config needs `kiplot.version`.") + return outputs diff --git a/kiplot/error.py b/kiplot/error.py index ff85bc66..473ddae3 100644 --- a/kiplot/error.py +++ b/kiplot/error.py @@ -5,3 +5,11 @@ KiPlot errors class KiPlotError(Exception): pass + + +class PlotError(KiPlotError): + pass + + +class KiPlotConfigurationError(KiPlotError): + pass diff --git a/kiplot/kiplot.py b/kiplot/kiplot.py index fcc35779..ef8fd246 100644 --- a/kiplot/kiplot.py +++ b/kiplot/kiplot.py @@ -2,49 +2,85 @@ Main Kiplot code """ -from datetime import datetime import os -from sys import exit -import operator -from shutil import which -from glob import glob -from subprocess import (call, run, PIPE, check_output, CalledProcessError, - STDOUT) -import logging -from distutils.version import StrictVersion import re +from sys import exit +from shutil import which +from subprocess import (run, PIPE) +from distutils.version import StrictVersion -from . import plot_config as PCfg -from . import error +from .misc import (PLOT_ERROR, NO_PCBNEW_MODULE, MISSING_TOOL, CMD_EESCHEMA_DO, URL_EESCHEMA_DO, NO_SCH_FILE, CORRUPTED_PCB, + EXIT_BAD_ARGS) +from .error import (PlotError) +from .pre_base import BasePreFlight from . import log -from . import misc logger = log.get_logger(__name__) try: import pcbnew - from pcbnew import GERBER_JOBFILE_WRITER except ImportError: # pragma: no cover log.init(False, False) logger.error("Failed to import pcbnew Python module." " Is KiCad installed?" " Do you need to add it to PYTHONPATH?") - exit(misc.NO_PCBNEW_MODULE) + exit(NO_PCBNEW_MODULE) -class PlotError(error.KiPlotError): - - pass +class GS(object): + """ + Class to keep the global settings. + Is a static class, just a placeholder for some global variables. + """ + pcb_file = None + out_dir = None + filter_file = None + debug_enabled = False + pcb_layers = None -def plot_error(msg): - logger.error(msg) - exit(misc.PLOT_ERROR) +class Layer(object): + """ A layer description """ + def __init__(self, id, is_inner, name): + self.id = id + self.is_inner = is_inner + self.name = name + self.suffix = None + self.desc = None + + def set_extra(self, suffix, desc): + self.suffix = suffix + self.desc = desc + + def __str__(self): + return "{} ({} '{}' {})".format(self.name, self.id, self.desc, self.suffix) -def level_debug(): - """ Determine if we are in debug mode """ - return logger.getEffectiveLevel() <= logging.DEBUG +def load_pcb_layers(): + """ Load layer names from the PCB """ + GS.pcb_layers = {} + with open(GS.pcb_file, "r") as pcb_file: + collect_layers = False + for line in pcb_file: + if collect_layers: + z = re.match(r'\s+\((\d+)\s+(\S+)', line) + if z: + res = z.groups() + # print(res[1]+'->'+res[0]) + GS.pcb_layers[res[1]] = int(res[0]) + else: + if re.search(r'^\s+\)$', line): + collect_layers = False + break + else: + if re.search(r'\s+\(layers', line): + collect_layers = True + + +def get_layer_id_from_pcb(name): + if GS.pcb_layers is None: + load_pcb_layers() + return GS.pcb_layers.get(name) def check_version(command, version): @@ -55,775 +91,102 @@ def check_version(command, version): if not z: logger.error('Unable to determine ' + command + ' version:\n' + result.stdout) - exit(misc.MISSING_TOOL) + exit(MISSING_TOOL) res = z.groups() if StrictVersion(res[0]) < StrictVersion(version): logger.error('Wrong version for `'+command+'` ('+res[0]+'), must be ' + version+' or newer.') - exit(misc.MISSING_TOOL) + exit(MISSING_TOOL) def check_script(cmd, url, version=None): if which(cmd) is None: logger.error('No `'+cmd+'` command found.\n' 'Please install it, visit: '+url) - exit(misc.MISSING_TOOL) + exit(MISSING_TOOL) if version is not None: check_version(cmd, version) -def check_eeschema_do(file): - check_script(misc.CMD_EESCHEMA_DO, misc.URL_EESCHEMA_DO, '1.4.0') - sch_file = os.path.splitext(file)[0] + '.sch' - if not os.path.isfile(sch_file): - logger.error('Missing schematic file: ' + sch_file) - exit(misc.NO_SCH_FILE) - return sch_file +def check_eeschema_do(): + check_script(CMD_EESCHEMA_DO, URL_EESCHEMA_DO, '1.4.0') + if not GS.sch_file: + logger.error('Missing schematic file') + exit(NO_SCH_FILE) -class Plotter(object): - """ - Main Plotter class - this is what will perform the plotting - """ +def load_board(): + try: + board = pcbnew.LoadBoard(GS.pcb_file) + if BasePreFlight.get_option('check_zone_fills'): + pcbnew.ZONE_FILLER(board).Fill(board.Zones()) + except OSError as e: + logger.error('Error loading PCB file. Currupted?') + logger.error(e) + exit(CORRUPTED_PCB) + assert board is not None + logger.debug("Board loaded") + return board - def __init__(self, cfg): - self.cfg = cfg - def plot(self, brd_file, target, invert, skip_pre): +def preflight_checks(skip_pre): + logger.debug("Preflight checks") - logger.debug("Starting plot of board {}".format(brd_file)) - self._preflight_checks(brd_file, skip_pre) - - n = len(target) - if n == 0 and invert: - # Skip all targets - logger.debug('Skipping all outputs') + if skip_pre is not None: + if skip_pre[0] == 'all': + logger.debug("Skipping all pre-flight actions") return - - board = None - for op in self.cfg.outputs: - - if (n == 0) or ((op.name in target) ^ invert): - logger.info('- %s (%s) [%s]' % (op.description, op.name, op.options.type)) - - output_dir = self._get_output_dir(op) - if (not self._output_is_schematic(op)) and (board is None): - board = self._load_board(brd_file) - - try: - if self._output_is_layer(op): - self._do_layer_plot(board, output_dir, op, brd_file) - elif self._output_is_drill(op): - self._do_drill_plot(board, output_dir, op) - elif self._output_is_position(op): - self._do_position_plot(board, output_dir, op) - elif self._output_is_bom(op): - self._do_bom(output_dir, op, brd_file) - elif self._output_is_sch_print(op): - self._do_sch_print(output_dir, op, brd_file) - elif self._output_is_pcb_print(op): - self._do_pcb_print(board, output_dir, op, brd_file) - elif self._output_is_step(op): - self._do_step(output_dir, op, brd_file) - else: # pragma no cover - # We shouldn't get here, means the above if is incomplete - plot_error("Don't know how to plot type "+op.options.type) - except PlotError as e: - plot_error("In section '"+op.name+"' ("+op.options.type+"): "+str(e)) - else: - logger.debug('Skipping %s output', op.name) - - def _load_board(self, brd_file): - try: - board = pcbnew.LoadBoard(brd_file) - if self.cfg.check_zone_fills: - pcbnew.ZONE_FILLER(board).Fill(board.Zones()) - except OSError as e: - logger.error('Error loading PCB file. Currupted?') - logger.error(e) - exit(misc.CORRUPTED_PCB) - assert board is not None - logger.debug("Board loaded") - return board - - def _preflight_checks(self, brd_file, skip_pre): - logger.debug("Preflight checks") - - if skip_pre is not None: - if skip_pre[0] == 'all': - logger.debug("Skipping all pre-flight actions") - return - else: - skip_list = skip_pre[0].split(',') - for skip in skip_list: - if skip == 'all': - logger.error('All can\'t be part of a list of actions ' - 'to skip. Use `--skip all`') - exit(misc.EXIT_BAD_ARGS) - elif skip == 'run_drc': - self.cfg.run_drc = False - logger.debug('Skipping run_drc') - elif skip == 'update_xml': - self.cfg.update_xml = False - logger.debug('Skipping update_xml') - elif skip == 'run_erc': - self.cfg.run_erc = False - logger.debug('Skipping run_erc') - elif skip == 'check_zone_fills': - self.cfg.check_zone_fills = False - logger.debug('Skipping run_erc') + else: + skip_list = skip_pre[0].split(',') + for skip in skip_list: + if skip == 'all': + logger.error('All can\'t be part of a list of actions ' + 'to skip. Use `--skip all`') + exit(EXIT_BAD_ARGS) + else: + if not BasePreFlight.is_registered(skip): + logger.error('Unknown preflight `{}`'.format(skip)) + exit(EXIT_BAD_ARGS) + o_pre = BasePreFlight.get_preflight(skip) + if not o_pre: + logger.warning('`{}` preflight is not in use, no need to skip'.format(skip)) else: - logger.error('Unknown action to skip: '+skip) - exit(misc.EXIT_BAD_ARGS) - # Create the filters file - filter_file = None - if (self.cfg.run_erc or self.cfg.run_drc) and self.cfg.filters: - filter_file = os.path.join(self.cfg.outdir, 'kiplot_errors.filter') - with open(filter_file, 'w') as f: - f.write(self.cfg.filters) - if self.cfg.run_erc: - self._run_erc(brd_file, filter_file) - if self.cfg.update_xml: - self._update_xml(brd_file) - if self.cfg.run_drc: - self._run_drc(brd_file, self.cfg.ignore_unconnected, filter_file) + logger.debug('Skipping `{}`'.format(skip)) + o_pre.disable() + BasePreFlight.run_enabled() - def _run_erc(self, brd_file, filter_file): - sch_file = check_eeschema_do(brd_file) - cmd = [misc.CMD_EESCHEMA_DO, 'run_erc'] - if filter_file: - cmd.extend(['-f', filter_file]) - cmd.extend([sch_file, self.cfg.outdir]) - # If we are in verbose mode enable debug in the child - if level_debug(): - cmd.insert(1, '-vv') - cmd.insert(1, '-r') - logger.info('- Running the ERC') - logger.debug('Executing: '+str(cmd)) - ret = call(cmd) - if ret: - if ret < 0: - logger.error('ERC errors: %d', -ret) - else: - logger.error('ERC returned %d', ret) - exit(misc.ERC_ERROR) - def _update_xml(self, brd_file): - sch_file = check_eeschema_do(brd_file) - cmd = [misc.CMD_EESCHEMA_DO, 'bom_xml', sch_file, self.cfg.outdir] - # If we are in verbose mode enable debug in the child - if level_debug(): - cmd.insert(1, '-vv') - cmd.insert(1, '-r') - logger.info('- Updating BoM in XML format') - logger.debug('Executing: '+str(cmd)) - ret = call(cmd) - if ret: - logger.error('Failed to update the BoM, error %d', ret) - exit(misc.BOM_ERROR) +def get_output_dir(o_dir): + # outdir is a combination of the config and output + outdir = os.path.join(GS.out_dir, o_dir) + # Create directory if needed + logger.debug("Output destination: {}".format(outdir)) + if not os.path.exists(outdir): + os.makedirs(outdir) + return outdir - def _run_drc(self, brd_file, ignore_unconnected, filter_file): - check_script(misc.CMD_PCBNEW_RUN_DRC, misc.URL_PCBNEW_RUN_DRC, '1.4.0') - cmd = [misc.CMD_PCBNEW_RUN_DRC, 'run_drc'] - if filter_file: - cmd.extend(['-f', filter_file]) - cmd.extend([brd_file, self.cfg.outdir]) - # If we are in verbose mode enable debug in the child - if level_debug(): - cmd.insert(1, '-vv') - cmd.insert(1, '-r') - if ignore_unconnected: - cmd.insert(1, '-i') - logger.info('- Running the DRC') - logger.debug('Executing: '+str(cmd)) - ret = call(cmd) - if ret: - if ret < 0: - logger.error('DRC errors: %d', -ret) - else: - logger.error('DRC returned %d', ret) - exit(misc.DRC_ERROR) - def _output_is_layer(self, output): - """ All the formats for 'PCB|Plot' """ - return output.options.type in [ - PCfg.OutputOptions.GERBER, - PCfg.OutputOptions.POSTSCRIPT, - PCfg.OutputOptions.DXF, - PCfg.OutputOptions.SVG, - PCfg.OutputOptions.PDF, - PCfg.OutputOptions.HPGL, - ] - - def _output_is_drill(self, output): - """ All the drill formats' """ - return output.options.type in [ - PCfg.OutputOptions.EXCELLON, - PCfg.OutputOptions.GERB_DRILL, - ] - - def _output_is_position(self, output): - return output.options.type == PCfg.OutputOptions.POSITION - - def _output_is_sch_print(self, output): - return output.options.type == PCfg.OutputOptions.PDF_SCH_PRINT - - def _output_is_pcb_print(self, output): - return output.options.type == PCfg.OutputOptions.PDF_PCB_PRINT - - def _output_is_step(self, output): - return output.options.type == PCfg.OutputOptions.STEP - - def _output_is_bom(self, output): - return output.options.type in [ - PCfg.OutputOptions.KIBOM, - PCfg.OutputOptions.IBOM, - ] - - def _output_is_schematic(self, output): - """ All the outputs involving the SCH and not the PCB """ - return self._output_is_bom(output) or self._output_is_sch_print(output) - - def _get_layer_plot_format(self, output): - """ - Gets the Pcbnew plot format for a given KiPlot output type - """ - - mapping = { - PCfg.OutputOptions.GERBER: pcbnew.PLOT_FORMAT_GERBER, - PCfg.OutputOptions.POSTSCRIPT: pcbnew.PLOT_FORMAT_POST, - PCfg.OutputOptions.HPGL: pcbnew.PLOT_FORMAT_HPGL, - PCfg.OutputOptions.PDF: pcbnew.PLOT_FORMAT_PDF, - PCfg.OutputOptions.DXF: pcbnew.PLOT_FORMAT_DXF, - PCfg.OutputOptions.SVG: pcbnew.PLOT_FORMAT_SVG, - } - - try: - return mapping[output.options.type] - except KeyError: - pass - - raise ValueError("Don't know how to translate plot type: {}" - .format(output.options.type)) - - def _do_layer_plot(self, board, output_dir, output, file_name): - - # fresh plot controller - plot_ctrl = pcbnew.PLOT_CONTROLLER(board) - # set up plot options for the whole output - self._configure_plot_ctrl(plot_ctrl, output, output_dir) - - po = plot_ctrl.GetPlotOptions() - layer_cnt = board.GetCopperLayerCount() - - # Gerber Job files aren't automagically created - # We need to assist KiCad - create_job = po.GetCreateGerberJobFile() - if create_job: - jobfile_writer = GERBER_JOBFILE_WRITER(board) - - plot_ctrl.SetColorMode(True) - - # plot every layer in the output - for l in output.layers: - - layer = l.layer - suffix = l.suffix - desc = l.desc - - # for inner layers, we can now check if the layer exists - if layer.is_inner: - if layer.layer < 1 or layer.layer >= layer_cnt - 1: - raise PlotError( - "Inner layer {} is not valid for this board" - .format(layer.layer)) - - # Set current layer - plot_ctrl.SetLayer(layer.layer) - - # Skipping NPTH is controlled by whether or not this is - # a copper layer - is_cu = pcbnew.IsCopperLayer(layer.layer) - po.SetSkipPlotNPTH_Pads(is_cu) - - plot_format = self._get_layer_plot_format(output) - - # Plot single layer to file - logger.debug("Opening plot file for layer {} ({}) {} {}" - .format(layer.layer, suffix, plot_format, desc)) - if not plot_ctrl.OpenPlotfile(suffix, plot_format, desc): - plot_error("OpenPlotfile failed!") - - logger.debug("Plotting layer {} to {}".format( - layer.layer, plot_ctrl.GetPlotFileName())) - plot_ctrl.PlotLayer() - plot_ctrl.ClosePlot() - if create_job: - jobfile_writer.AddGbrFile(layer.layer, os.path.basename( - plot_ctrl.GetPlotFileName())) - - if create_job: - base_fn = os.path.join( - os.path.dirname(plot_ctrl.GetPlotFileName()), - os.path.basename(file_name)) - base_fn = os.path.splitext(base_fn)[0] - job_fn = base_fn+'-job.gbrjob' - jobfile_writer.CreateJobFile(job_fn) - - def _configure_excellon_drill_writer(self, board, offset, options): - - drill_writer = pcbnew.EXCELLON_WRITER(board) - - to = options.type_options - - mirror_y = to.mirror_y_axis - minimal_header = to.minimal_header - - merge_npth = to.pth_and_npth_single_file - zeros_format = pcbnew.EXCELLON_WRITER.DECIMAL_FORMAT - - drill_writer.SetOptions(mirror_y, minimal_header, offset, merge_npth) - drill_writer.SetFormat(to.metric_units, zeros_format) - - return drill_writer - - def _configure_gerber_drill_writer(self, board, offset, options): - - drill_writer = pcbnew.GERBER_WRITER(board) - - # hard coded in UI? - drill_writer.SetFormat(5) - drill_writer.SetOptions(offset) - - return drill_writer - - def _do_drill_plot(self, board, output_dir, output): - - to = output.options.type_options - - # dialog_gendrill.cpp:357 - if to.use_aux_axis_as_origin: - offset = board.GetAuxOrigin() +def generate_outputs(outputs, target, invert, skip_pre): + logger.debug("Starting outputs for board {}".format(GS.pcb_file)) + preflight_checks(skip_pre) + # Check if all must be skipped + n = len(target) + if n == 0 and invert: + # Skip all targets + logger.debug('Skipping all outputs') + return + # Generate outputs + board = None + for out in outputs: + if (n == 0) or ((out.get_name() in target) ^ invert): + logger.info('- '+str(out)) + # Should we load the PCB? + if out.is_pcb() and (board is None): + board = load_board() + try: + out.run(get_output_dir(out.get_outdir()), board) + except PlotError as e: + logger.error("In output `"+str(out)+"`: "+str(e)) + exit(PLOT_ERROR) else: - offset = pcbnew.wxPoint(0, 0) - - if output.options.type == PCfg.OutputOptions.EXCELLON: - drill_writer = self._configure_excellon_drill_writer( - board, offset, output.options) - elif output.options.type == PCfg.OutputOptions.GERB_DRILL: - drill_writer = self._configure_gerber_drill_writer( - board, offset, output.options) - else: - plot_error("Can't make a writer for type "+output.options.type) - - gen_drill = True - gen_map = to.generate_map - gen_report = to.generate_report - - if gen_drill: - logger.debug("Generating drill files in "+output_dir) - - if gen_map: - drill_writer.SetMapFileFormat(to.map_options.type) - logger.debug("Generating drill map type {} in {}" - .format(to.map_options.type, output_dir)) - - drill_writer.CreateDrillandMapFilesSet(output_dir, gen_drill, gen_map) - - if gen_report: - drill_report_file = os.path.join(output_dir, - to.report_options.filename) - logger.debug("Generating drill report: "+drill_report_file) - - drill_writer.GenDrillReportFile(drill_report_file) - - def _do_position_plot_ascii(self, board, output_dir, output, columns, - modulesStr, maxSizes): - to = output.options.type_options - name = os.path.splitext(os.path.basename(board.GetFileName()))[0] - - topf = None - botf = None - bothf = None - if to.separate_files_for_front_and_back: - topf = open(os.path.join(output_dir, "{}-top.pos".format(name)), 'w') - botf = open(os.path.join(output_dir, "{}-bottom.pos".format(name)), - 'w') - else: - bothf = open(os.path.join(output_dir, "{}-both.pos").format(name), 'w') - - files = [f for f in [topf, botf, bothf] if f is not None] - for f in files: - f.write('### Module positions - created on {} ###\n'.format( - datetime.now().strftime("%a %d %b %Y %X %Z") - )) - f.write('### Printed by KiPlot\n') - unit = {'millimeters': 'mm', - 'inches': 'in'}[to.units] - f.write('## Unit = {}, Angle = deg.\n'.format(unit)) - - if topf is not None: - topf.write('## Side : top\n') - if botf is not None: - botf.write('## Side : bottom\n') - if bothf is not None: - bothf.write('## Side : both\n') - - for f in files: - f.write('# ') - for idx, col in enumerate(columns): - if idx > 0: - f.write(" ") - f.write("{0: <{width}}".format(col, width=maxSizes[idx])) - f.write('\n') - - # Account for the "# " at the start of the comment column - maxSizes[0] = maxSizes[0] + 2 - - for m in modulesStr: - fle = bothf - if fle is None: - if m[-1] == "top": - fle = topf - else: - fle = botf - for idx, col in enumerate(m): - if idx > 0: - fle.write(" ") - fle.write("{0: <{width}}".format(col, width=maxSizes[idx])) - fle.write("\n") - - for f in files: - f.write("## End\n") - - if topf is not None: - topf.close() - if botf is not None: - botf.close() - if bothf is not None: - bothf.close() - - def _do_position_plot_csv(self, board, output_dir, output, columns, - modulesStr): - to = output.options.type_options - name = os.path.splitext(os.path.basename(board.GetFileName()))[0] - - topf = None - botf = None - bothf = None - if to.separate_files_for_front_and_back: - topf = open(os.path.join(output_dir, "{}-top-pos.csv".format(name)), - 'w') - botf = open(os.path.join(output_dir, "{}-bottom-pos.csv".format(name)), - 'w') - else: - bothf = open(os.path.join(output_dir, "{}-both-pos.csv").format(name), - 'w') - - files = [f for f in [topf, botf, bothf] if f is not None] - - for f in files: - f.write(",".join(columns)) - f.write("\n") - - for m in modulesStr: - fle = bothf - if fle is None: - if m[-1] == "top": - fle = topf - else: - fle = botf - fle.write(",".join('"{}"'.format(e) for e in m)) - fle.write("\n") - - if topf is not None: - topf.close() - if botf is not None: - botf.close() - if bothf is not None: - bothf.close() - - def _do_position_plot(self, board, output_dir, output): - to = output.options.type_options - - columns = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"] - colcount = len(columns) - - # Note: the parser already checked the units are milimeters or inches - conv = 1.0 - if to.units == 'millimeters': - conv = 1.0 / pcbnew.IU_PER_MM - else: # to.units == 'inches': - conv = 0.001 / pcbnew.IU_PER_MILS - - # Format all strings - modules = [] - for m in sorted(board.GetModules(), - key=operator.methodcaller('GetReference')): - if (to.only_smd and m.GetAttributes() == 1) or not to.only_smd: - center = m.GetCenter() - # See PLACE_FILE_EXPORTER::GenPositionData() in - # export_footprints_placefile.cpp for C++ version of this. - modules.append([ - "{}".format(m.GetReference()), - "{}".format(m.GetValue()), - "{}".format(m.GetFPID().GetLibItemName()), - "{:.4f}".format(center.x * conv), - "{:.4f}".format(-center.y * conv), - "{:.4f}".format(m.GetOrientationDegrees()), - "{}".format("bottom" if m.IsFlipped() else "top") - ]) - - # Find max width for all columns - maxlengths = [0] * colcount - for row in range(len(modules)): - for col in range(colcount): - maxlengths[col] = max(maxlengths[col], len(modules[row][col])) - - # Note: the parser already checked the format is ASCII or CSV - if to.format.lower() == 'ascii': - self._do_position_plot_ascii(board, output_dir, output, columns, - modules, maxlengths) - else: # if to.format.lower() == 'csv': - self._do_position_plot_csv(board, output_dir, output, columns, - modules) - - def _do_sch_print(self, output_dir, output, brd_file): - sch_file = check_eeschema_do(brd_file) - cmd = [misc.CMD_EESCHEMA_DO, 'export', '--all_pages', - '--file_format', 'pdf', sch_file, output_dir] - if level_debug(): - cmd.insert(1, '-vv') - cmd.insert(1, '-r') - logger.debug('Executing: '+str(cmd)) - ret = call(cmd) - if ret: - logger.error(misc.CMD_EESCHEMA_DO+' returned %d', ret) - exit(misc.PDF_SCH_PRINT) - to = output.options.type_options - if to.output: - cur = os.path.abspath(os.path.join(output_dir, os.path.splitext(os.path.basename(brd_file))[0]) + '.pdf') - new = os.path.abspath(os.path.join(output_dir, to.output)) - logger.debug('Moving '+cur+' -> '+new) - os.rename(cur, new) - - def _do_step(self, output_dir, op, brd_file): - to = op.options.type_options - # Output file name - output = to.output - if output is None: - output = os.path.splitext(os.path.basename(brd_file))[0]+'.step' - output = os.path.abspath(os.path.join(output_dir, output)) - # Make units explicit - if to.metric_units: - units = 'mm' - else: - units = 'in' - # Base command with overwrite - cmd = [misc.KICAD2STEP, '-o', output, '-f'] - # Add user options - if to.no_virtual: - cmd.append('--no-virtual') - if to.min_distance is not None: - cmd.extend(['--min-distance', "{}{}".format(to.min_distance, units)]) - if to.origin == 'drill': - cmd.append('--drill-origin') - elif to.origin == 'grid': - cmd.append('--grid-origin') - else: - cmd.extend(['--user-origin', "{}{}".format(to.origin.replace(',', 'x'), units)]) - # The board - cmd.append(brd_file) - # Execute and inform is successful - logger.debug('Executing: '+str(cmd)) - try: - cmd_output = check_output(cmd, stderr=STDOUT) - except CalledProcessError as e: # pragma: no cover - # Current kicad2step always returns 0!!!! - # This is why I'm excluding it from coverage - logger.error('Failed to create Step file, error %d', e.returncode) - if e.output: - logger.debug('Output from command: '+e.output.decode()) - exit(misc.KICAD2STEP_ERR) - logger.debug('Output from command:\n'+cmd_output.decode()) - - def _do_pcb_print(self, board, output_dir, output, brd_file): - check_script(misc.CMD_PCBNEW_PRINT_LAYERS, - misc.URL_PCBNEW_PRINT_LAYERS, '1.4.1') - to = output.options.type_options - # Verify the inner layers - layer_cnt = board.GetCopperLayerCount() - for l in output.layers: - layer = l.layer - # for inner layers, we can now check if the layer exists - if layer.is_inner: - if layer.layer < 1 or layer.layer >= layer_cnt - 1: - raise PlotError( - "Inner layer {} is not valid for this board" - .format(layer.layer)) - cmd = [misc.CMD_PCBNEW_PRINT_LAYERS, 'export', - '--output_name', to.output_name] - if self.cfg.check_zone_fills: - cmd.append('-f') - cmd.extend([brd_file, output_dir]) - if level_debug(): - cmd.insert(1, '-vv') - cmd.insert(1, '-r') - # Add the layers - for l in output.layers: - cmd.append(l.layer.name) - logger.debug('Executing: '+str(cmd)) - ret = call(cmd) - if ret: - logger.error(misc.CMD_PCBNEW_PRINT_LAYERS+' returned %d', ret) - exit(misc.PDF_PCB_PRINT) - - def _do_bom(self, output_dir, output, brd_file): - if output.options.type == 'kibom': - self._do_kibom(output_dir, output, brd_file) - else: - self._do_ibom(output_dir, output, brd_file) - - def _do_kibom(self, output_dir, output, brd_file): - check_script(misc.CMD_KIBOM, misc.URL_KIBOM) - to = output.options.type_options - format = to.format.lower() - prj = os.path.splitext(os.path.relpath(brd_file))[0] - logger.debug('Doing BoM, format '+format+' prj: '+prj) - cmd = [misc.CMD_KIBOM, prj+'.xml', - os.path.join(output_dir, os.path.basename(prj))+'.'+format] - logger.debug('Running: '+str(cmd)) - try: - cmd_output = check_output(cmd, stderr=STDOUT) - except CalledProcessError as e: - logger.error('Failed to create BoM, error %d', e.returncode) - if e.output: - logger.debug('Output from command: '+e.output.decode()) - exit(misc.BOM_ERROR) - for f in glob(os.path.join(output_dir, prj)+'*.tmp'): - os.remove(f) - logger.debug('Output from command:\n'+cmd_output.decode()) - - def _do_ibom(self, output_dir, output, brd_file): - check_script(misc.CMD_IBOM, misc.URL_IBOM) - prj = os.path.splitext(os.path.relpath(brd_file))[0] - logger.debug('Doing Interactive BoM, prj: '+prj) - # Tell ibom we don't want to use the screen - os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = '' - cmd = [misc.CMD_IBOM, brd_file, - '--dest-dir', output_dir, - '--no-browser', ] - to = output.options.type_options - if to.blacklist: - cmd.append('--blacklist') - cmd.append(to.blacklist) - if to.name_format: - cmd.append('--name-format') - cmd.append(to.name_format) - logger.debug('Running: '+str(cmd)) - try: - cmd_output = check_output(cmd, stderr=STDOUT) - except CalledProcessError as e: - logger.error('Failed to create BoM, error %d', e.returncode) - if e.output: - logger.debug('Output from command: '+e.output.decode()) - exit(misc.BOM_ERROR) - logger.debug('Output from command:\n'+cmd_output.decode()+'\n') - - def _configure_gerber_opts(self, po, output): - - # true if gerber - po.SetUseGerberAttributes(True) - - assert(output.options.type == PCfg.OutputOptions.GERBER) - gerb_opts = output.options.type_options - - po.SetSubtractMaskFromSilk(gerb_opts.subtract_mask_from_silk) - po.SetUseGerberProtelExtensions(gerb_opts.use_protel_extensions) - po.SetGerberPrecision(gerb_opts.gerber_precision) - po.SetCreateGerberJobFile(gerb_opts.create_gerber_job_file) - - po.SetUseGerberAttributes(gerb_opts.use_gerber_x2_attributes) - po.SetIncludeGerberNetlistInfo(gerb_opts.use_gerber_net_attributes) - - def _configure_hpgl_opts(self, po, output): - - assert(output.options.type == PCfg.OutputOptions.HPGL) - hpgl_opts = output.options.type_options - - po.SetHPGLPenDiameter(hpgl_opts.pen_width) - - def _configure_ps_opts(self, po, output): - - assert(output.options.type == PCfg.OutputOptions.POSTSCRIPT) - ps_opts = output.options.type_options - - po.SetWidthAdjust(ps_opts.width_adjust) - po.SetFineScaleAdjustX(ps_opts.scale_adjust_x) - po.SetFineScaleAdjustX(ps_opts.scale_adjust_y) - po.SetA4Output(ps_opts.a4_output) - - def _configure_dxf_opts(self, po, output): - - assert(output.options.type == PCfg.OutputOptions.DXF) - dxf_opts = output.options.type_options - - po.SetDXFPlotPolygonMode(dxf_opts.polygon_mode) - - def _get_output_dir(self, output): - - # outdir is a combination of the config and output - outdir = os.path.join(self.cfg.outdir, output.outdir) - - logger.debug("Output destination: {}".format(outdir)) - if not os.path.exists(outdir): - os.makedirs(outdir) - - return outdir - - def _configure_plot_ctrl(self, plot_ctrl, output, output_dir): - - logger.debug("Configuring plot controller for output") - - po = plot_ctrl.GetPlotOptions() - po.SetOutputDirectory(output_dir) - - opts = output.options.type_options - - po.SetLineWidth(opts.line_width) - - po.SetAutoScale(opts.auto_scale) - po.SetScale(opts.scaling) - - po.SetMirror(opts.mirror_plot) - po.SetNegative(opts.negative_plot) - - po.SetPlotFrameRef(opts.plot_sheet_reference) - po.SetPlotReference(opts.plot_footprint_refs) - po.SetPlotValue(opts.plot_footprint_values) - po.SetPlotInvisibleText(opts.force_plot_invisible_refs_vals) - - po.SetExcludeEdgeLayer(opts.exclude_edge_layer) - po.SetPlotPadsOnSilkLayer(not opts.exclude_pads_from_silkscreen) - po.SetUseAuxOrigin(opts.use_aux_axis_as_origin) - - po.SetPlotViaOnMaskLayer(not opts.tent_vias) - - # in general, false, but gerber will set it back later - po.SetUseGerberAttributes(False) - # Only useful for gerber outputs - po.SetCreateGerberJobFile(False) - - if output.options.type == PCfg.OutputOptions.GERBER: - self._configure_gerber_opts(po, output) - elif output.options.type == PCfg.OutputOptions.POSTSCRIPT: - self._configure_ps_opts(po, output) - elif output.options.type == PCfg.OutputOptions.DXF: - self._configure_dxf_opts(po, output) - elif output.options.type == PCfg.OutputOptions.HPGL: - self._configure_hpgl_opts(po, output) - - po.SetDrillMarksType(opts.drill_marks) - - # We'll come back to this on a per-layer basis - po.SetSkipPlotNPTH_Pads(False) + logger.debug('Skipping `%s` output', str(out)) diff --git a/kiplot/out_any_drill.py b/kiplot/out_any_drill.py new file mode 100644 index 00000000..4d7a52ad --- /dev/null +++ b/kiplot/out_any_drill.py @@ -0,0 +1,88 @@ +import os +from pcbnew import (PLOT_FORMAT_HPGL, PLOT_FORMAT_POST, PLOT_FORMAT_GERBER, PLOT_FORMAT_DXF, PLOT_FORMAT_SVG, + PLOT_FORMAT_PDF, wxPoint) +from .out_base import BaseOutput +from .error import KiPlotConfigurationError +from . import log + +logger = log.get_logger(__name__) + + +class AnyDrill(BaseOutput): + def __init__(self, name, type, description): + super(AnyDrill, self).__init__(name, type, description) + # Options + self.use_aux_axis_as_origin = False + self._map = None + self._report = None + # Mappings to KiCad values + self._map_map = { + 'hpgl': PLOT_FORMAT_HPGL, + 'ps': PLOT_FORMAT_POST, + 'gerber': PLOT_FORMAT_GERBER, + 'dxf': PLOT_FORMAT_DXF, + 'svg': PLOT_FORMAT_SVG, + 'pdf': PLOT_FORMAT_PDF + } + + @property + def map(self): + return self._map + + @map.setter + def map(self, val): + if val is None: + raise KiPlotConfigurationError("Empty drill `map` section") + # Setting from a dict + if isinstance(val, dict): + if 'type' not in val: + raise KiPlotConfigurationError("drill `map` must contain a `type`") + type = val['type'] + if not isinstance(type, str): + raise KiPlotConfigurationError("drill `map` `type` must be a string") + val = type + if val not in self._map_map: + raise KiPlotConfigurationError("Unknown drill `map` `type`: {}".format(val)) + self._map = val + + @property + def report(self): + return self._report + + @report.setter + def report(self, val): + # Setting from a dict + if isinstance(val, dict): + if 'filename' not in val: + raise KiPlotConfigurationError("drill `report` must contain a `filename`") + filename = val['filename'] + if not isinstance(filename, str): + raise KiPlotConfigurationError("drill `report` `filename` must be a string") + val = filename + self._report = val + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + if self._map: + self._map = self._map_map[self._map] + + def run(self, output_dir, board): + # dialog_gendrill.cpp:357 + if self.use_aux_axis_as_origin: + offset = board.GetAuxOrigin() + else: + offset = wxPoint(0, 0) + drill_writer = self._configure_writer(board, offset) + + logger.debug("Generating drill files in "+output_dir) + gen_map = self._map is not None + if gen_map: + drill_writer.SetMapFileFormat(self._map) + logger.debug("Generating drill map type {} in {}".format(self._map, output_dir)) + # We always generate the drill file + drill_writer.CreateDrillandMapFilesSet(output_dir, True, gen_map) + + if self._report is not None: + drill_report_file = os.path.join(output_dir, self._report) + logger.debug("Generating drill report: "+drill_report_file) + drill_writer.GenDrillReportFile(drill_report_file) diff --git a/kiplot/out_any_layer.py b/kiplot/out_any_layer.py new file mode 100644 index 00000000..9fa3083e --- /dev/null +++ b/kiplot/out_any_layer.py @@ -0,0 +1,145 @@ +import os +from pcbnew import (GERBER_JOBFILE_WRITER, PCB_PLOT_PARAMS, FromMM, PLOT_CONTROLLER, IsCopperLayer) +from .out_base import (BaseOutput) +from .error import (PlotError, KiPlotConfigurationError) +from .kiplot import (GS) +from . import log + +logger = log.get_logger(__name__) +AUTO_SCALE = 0 + + +class AnyLayer(BaseOutput): + def __init__(self, name, type, description): + super(AnyLayer, self).__init__(name, type, description) + # Options + self.exclude_edge_layer = True + self.exclude_pads_from_silkscreen = False + self.plot_sheet_reference = False + self.plot_footprint_refs = True + self.plot_footprint_values = True + self.force_plot_invisible_refs_vals = False + self.tent_vias = True + self.check_zone_fills = True + # Mappings to KiCad values + self._drill_marks_map = { + 'none': PCB_PLOT_PARAMS.NO_DRILL_SHAPE, + 'small': PCB_PLOT_PARAMS.SMALL_DRILL_SHAPE, + 'full': PCB_PLOT_PARAMS.FULL_DRILL_SHAPE, + } + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + # We need layers + if not self._layers: + raise KiPlotConfigurationError("Missing `layers` list") + + def _configure_plot_ctrl(self, po, output_dir): + logger.debug("Configuring plot controller for output") + po.SetOutputDirectory(output_dir) + po.SetLineWidth(FromMM(self.get_line_width())) + # Scaling/Autoscale + scaling = self.get_scaling() + if scaling == AUTO_SCALE: + po.SetAutoScale(True) + po.SetScale(1) + else: + po.SetAutoScale(False) + po.SetScale(scaling) + po.SetMirror(self.get_mirror_plot()) + po.SetNegative(self.get_negative_plot()) + po.SetPlotFrameRef(self.plot_sheet_reference) + po.SetPlotReference(self.plot_footprint_refs) + po.SetPlotValue(self.plot_footprint_values) + po.SetPlotInvisibleText(self.force_plot_invisible_refs_vals) + po.SetExcludeEdgeLayer(self.exclude_edge_layer) + po.SetPlotPadsOnSilkLayer(not self.exclude_pads_from_silkscreen) + po.SetUseAuxOrigin(self.get_use_aux_axis_as_origin()) + po.SetPlotViaOnMaskLayer(not self.tent_vias) + # in general, false, but gerber will set it back later + po.SetUseGerberAttributes(False) + # Only useful for gerber outputs + po.SetCreateGerberJobFile(False) + # How we draw drill marks + po.SetDrillMarksType(self.get_drill_marks()) + # We'll come back to this on a per-layer basis + po.SetSkipPlotNPTH_Pads(False) + + def get_plot_format(self): + return self._plot_format + + def run(self, output_dir, board): + # fresh plot controller + plot_ctrl = PLOT_CONTROLLER(board) + # set up plot options for the whole output + po = plot_ctrl.GetPlotOptions() + self._configure_plot_ctrl(po, output_dir) + + layer_cnt = board.GetCopperLayerCount() + + # Gerber Job files aren't automagically created + # We need to assist KiCad + create_job = po.GetCreateGerberJobFile() + if create_job: + jobfile_writer = GERBER_JOBFILE_WRITER(board) + + plot_ctrl.SetColorMode(True) + + # plot every layer in the output + for l in self._layers: + suffix = l.suffix + desc = l.desc + id = l.id + # for inner layers, we can now check if the layer exists + if l.is_inner: + if id < 1 or id >= layer_cnt - 1: + raise PlotError("Inner layer `{}` is not valid for this board".format(l)) + # Set current layer + plot_ctrl.SetLayer(id) + # Skipping NPTH is controlled by whether or not this is + # a copper layer + is_cu = IsCopperLayer(id) + po.SetSkipPlotNPTH_Pads(is_cu) + + plot_format = self.get_plot_format() + + # Plot single layer to file + logger.debug("Opening plot file for layer `{}` format `{}`".format(l, plot_format)) + if not plot_ctrl.OpenPlotfile(suffix, plot_format, desc): + raise PlotError("OpenPlotfile failed!") + + logger.debug("Plotting layer `{}` to `{}`".format(l, plot_ctrl.GetPlotFileName())) + plot_ctrl.PlotLayer() + plot_ctrl.ClosePlot() + if create_job: + jobfile_writer.AddGbrFile(id, os.path.basename(plot_ctrl.GetPlotFileName())) + + if create_job: + base_fn = os.path.join( + os.path.dirname(plot_ctrl.GetPlotFileName()), + os.path.basename(GS.pcb_file)) + base_fn = os.path.splitext(base_fn)[0] + job_fn = base_fn+'-job.gbrjob' + jobfile_writer.CreateJobFile(job_fn) + + # Default values + # We concentrate all the KiCad plot initialization in one place. + # Here we provide default values for settings not contained in an output object + # TODO: avoid them? + def get_line_width(self): + return self.line_width if 'line_width' in self.__dict__ else 0 + + def get_scaling(self): + return self.scaling if 'scaling' in self.__dict__ else 1 + + def get_mirror_plot(self): + return self.mirror_plot if 'mirror_plot' in self.__dict__ else False + + def get_negative_plot(self): + return self.negative_plot if 'negative_plot' in self.__dict__ else False + + def get_use_aux_axis_as_origin(self): + return self.use_aux_axis_as_origin if 'use_aux_axis_as_origin' in self.__dict__ else False + + def get_drill_marks(self): + return self.drill_marks if '_drill_marks' in self.__dict__ else PCB_PLOT_PARAMS.NO_DRILL_SHAPE diff --git a/kiplot/out_base.py b/kiplot/out_base.py new file mode 100644 index 00000000..066d2784 --- /dev/null +++ b/kiplot/out_base.py @@ -0,0 +1,82 @@ +import inspect +from .error import KiPlotConfigurationError +from . import log + +logger = log.get_logger(__name__) + + +def filter(v): + return not (callable(v) or inspect.isclass(v) or isinstance(v, (dict, list))) + + +class BaseOutput(object): + _registered = {} + + def __init__(self, name, type, description): + self._name = name + self._type = type + self._description = description + self._sch_related = False + + def _perform_config_mapping(self): + """ Map the options to class attributes """ + attrs = dict(inspect.getmembers(self, filter)) + for k, v in self._options.items(): + # Map known attributes and avoid mapping private ones + if (k[0] == '_') or (k not in attrs): + # raise KiPlotConfigurationError("Unknown option `{}` {}".format(k, attrs)) + logger.warning("Unknown option `{}`".format(k)) + continue + # Check the data type + cur_val = self.__getattribute__(k) + if isinstance(cur_val, bool) and not isinstance(v, bool): + raise KiPlotConfigurationError("Option `{}` must be true/false".format(k)) + if isinstance(cur_val, (int, float)) and not isinstance(v, (int, float)): + raise KiPlotConfigurationError("Option `{}` must be a number".format(k)) + if isinstance(cur_val, str) and not isinstance(v, str): + raise KiPlotConfigurationError("Option `{}` must be a string".format(k)) + if isinstance(v, list): + raise KiPlotConfigurationError("list not yet supported for `{}`".format(k)) + # Seems to be ok, map it + setattr(self, k, v) + + def config(self, outdir, options, layers): + self._outdir = outdir + self._options = options + self._layers = layers + if options: + self._perform_config_mapping() + + @staticmethod + def register(name, aclass): + BaseOutput._registered[name] = aclass + + @staticmethod + def is_registered(name): + return name in BaseOutput._registered + + @staticmethod + def get_class_for(name): + return BaseOutput._registered[name] + + def __str__(self): + return "'{}' ({}) [{}]".format(self._description, self._name, self._type) + + def is_sch(self): + """ True for outputs that works on the schematic """ + return self._sch_related + + def is_pcb(self): + """ True for outputs that works on the PCB """ + return not self._sch_related + + # These get_* aren't really needed. + # _* members aren't supposed to be used by the user, not the code. + def get_name(self): + return self._name + + def get_outdir(self): + return self._outdir + + def run(self, output_dir, board): # pragma: no cover + logger.error("The run member for the class for the output type `{}` isn't implemented".format(self._type)) diff --git a/kiplot/out_dxf.py b/kiplot/out_dxf.py new file mode 100644 index 00000000..83b85034 --- /dev/null +++ b/kiplot/out_dxf.py @@ -0,0 +1,37 @@ +from pcbnew import PLOT_FORMAT_DXF +from .error import KiPlotConfigurationError +from .out_base import (BaseOutput) +from .out_any_layer import (AnyLayer) + + +class DXF(AnyLayer): + def __init__(self, name, type, description): + super(DXF, self).__init__(name, type, description) + self._plot_format = PLOT_FORMAT_DXF + # Options + self.use_aux_axis_as_origin = False + self._drill_marks = 'full' + self.polygon_mode = True + self.sketch_plot = True + + @property + def drill_marks(self): + return self._drill_marks + + @drill_marks.setter + def drill_marks(self, val): + if val not in self._drill_marks_map: + raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val)) + self._drill_marks = val + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + self._drill_marks = self._drill_marks_map[self._drill_marks] + + def _configure_plot_ctrl(self, po, output_dir): + super()._configure_plot_ctrl(po, output_dir) + po.SetDXFPlotPolygonMode(self.polygon_mode) + + +# Register it +BaseOutput.register('dxf', DXF) diff --git a/kiplot/out_excellon.py b/kiplot/out_excellon.py new file mode 100644 index 00000000..1a39a98f --- /dev/null +++ b/kiplot/out_excellon.py @@ -0,0 +1,22 @@ +from pcbnew import (EXCELLON_WRITER) +from .out_base import (BaseOutput) +from .out_any_drill import (AnyDrill) + + +class Excellon(AnyDrill): + def __init__(self, name, type, description): + super(Excellon, self).__init__(name, type, description) + self.metric_units = True + self.pth_and_npth_single_file = True + self.minimal_header = False + self.mirror_y_axis = False + + def _configure_writer(self, board, offset): + drill_writer = EXCELLON_WRITER(board) + drill_writer.SetOptions(self.mirror_y_axis, self.minimal_header, offset, self.pth_and_npth_single_file) + drill_writer.SetFormat(self.metric_units, EXCELLON_WRITER.DECIMAL_FORMAT) + return drill_writer + + +# Register it +BaseOutput.register('excellon', Excellon) diff --git a/kiplot/out_gerb_drill.py b/kiplot/out_gerb_drill.py new file mode 100644 index 00000000..b4bae5cb --- /dev/null +++ b/kiplot/out_gerb_drill.py @@ -0,0 +1,19 @@ +from pcbnew import (GERBER_WRITER) +from .out_base import (BaseOutput) +from .out_any_drill import (AnyDrill) + + +class GerbDrill(AnyDrill): + def __init__(self, name, type, description): + super(GerbDrill, self).__init__(name, type, description) + + def _configure_writer(self, board, offset): + drill_writer = GERBER_WRITER(board) + # hard coded in UI? + drill_writer.SetFormat(5) + drill_writer.SetOptions(offset) + return drill_writer + + +# Register it +BaseOutput.register('gerb_drill', GerbDrill) diff --git a/kiplot/out_gerber.py b/kiplot/out_gerber.py new file mode 100644 index 00000000..3afed001 --- /dev/null +++ b/kiplot/out_gerber.py @@ -0,0 +1,43 @@ +from pcbnew import (PLOT_FORMAT_GERBER) +from .out_base import (BaseOutput) +from .out_any_layer import (AnyLayer) +from .error import KiPlotConfigurationError + + +class Gerber(AnyLayer): + def __init__(self, name, type, description): + super(Gerber, self).__init__(name, type, description) + self._plot_format = PLOT_FORMAT_GERBER + # Options + self.use_aux_axis_as_origin = False + self.line_width = 0.1 + self.subtract_mask_from_silk = False + self.use_protel_extensions = False + self._gerber_precision = 4.6 + self.create_gerber_job_file = True + self.use_gerber_x2_attributes = True + self.use_gerber_net_attributes = True + + @property + def gerber_precision(self): + return self._gerber_precision + + @gerber_precision.setter + def gerber_precision(self, val): + if val != 4.5 and val != 4.6: + raise KiPlotConfigurationError("`gerber_precision` must be 4.5 or 4.6") + self._gerber_precision = val + + def _configure_plot_ctrl(self, po, output_dir): + super()._configure_plot_ctrl(po, output_dir) + po.SetUseGerberAttributes(True) + po.SetSubtractMaskFromSilk(self.subtract_mask_from_silk) + po.SetUseGerberProtelExtensions(self.use_protel_extensions) + po.SetGerberPrecision(5 if self.gerber_precision == 4.5 else 6) + po.SetCreateGerberJobFile(self.create_gerber_job_file) + po.SetUseGerberAttributes(self.use_gerber_x2_attributes) + po.SetIncludeGerberNetlistInfo(self.use_gerber_net_attributes) + + +# Register it +BaseOutput.register('gerber', Gerber) diff --git a/kiplot/out_hpgl.py b/kiplot/out_hpgl.py new file mode 100644 index 00000000..711fafc7 --- /dev/null +++ b/kiplot/out_hpgl.py @@ -0,0 +1,38 @@ +from pcbnew import (PLOT_FORMAT_HPGL) +from .out_base import (BaseOutput) +from .out_any_layer import (AnyLayer) +from .error import KiPlotConfigurationError + + +class HPGL(AnyLayer): + def __init__(self, name, type, description): + super(HPGL, self).__init__(name, type, description) + self._plot_format = PLOT_FORMAT_HPGL + # Options + self.mirror_plot = False + self.sketch_plot = True + self.scaling = 0 + self._drill_marks = 'full' + self.pen_width = 0.5 + + @property + def drill_marks(self): + return self._drill_marks + + @drill_marks.setter + def drill_marks(self, val): + if val not in self._drill_marks_map: + raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val)) + self._drill_marks = val + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + self._drill_marks = self._drill_marks_map[self._drill_marks] + + def _configure_plot_ctrl(self, po, output_dir): + super()._configure_plot_ctrl(po, output_dir) + po.SetHPGLPenDiameter(self.pen_width) + + +# Register it +BaseOutput.register('hpgl', HPGL) diff --git a/kiplot/out_ibom.py b/kiplot/out_ibom.py new file mode 100644 index 00000000..13d5742a --- /dev/null +++ b/kiplot/out_ibom.py @@ -0,0 +1,43 @@ +import os +from subprocess import (check_output, STDOUT, CalledProcessError) +from .out_base import (BaseOutput) +from .misc import (CMD_IBOM, URL_IBOM, BOM_ERROR) +from .kiplot import (GS, check_script) +from . import log + +logger = log.get_logger(__name__) + + +class IBoM(BaseOutput): + def __init__(self, name, type, description): + super(IBoM, self).__init__(name, type, description) + self._sch_related = True + # Options + self.blacklist = '' + self.name_format = '' + + def run(self, output_dir, board): + check_script(CMD_IBOM, URL_IBOM) + logger.debug('Doing Interactive BoM') + # Tell ibom we don't want to use the screen + os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = '' + cmd = [CMD_IBOM, GS.pcb_file, '--dest-dir', output_dir, '--no-browser', ] + if self.blacklist: + cmd.append('--blacklist') + cmd.append(self.blacklist) + if self.name_format: + cmd.append('--name-format') + cmd.append(self.name_format) + logger.debug('Running: '+str(cmd)) + try: + cmd_output = check_output(cmd, stderr=STDOUT) + except CalledProcessError as e: + logger.error('Failed to create BoM, error %d', e.returncode) + if e.output: + logger.debug('Output from command: '+e.output.decode()) + exit(BOM_ERROR) + logger.debug('Output from command:\n'+cmd_output.decode()+'\n') + + +# Register it +BaseOutput.register('ibom', IBoM) diff --git a/kiplot/out_kibom.py b/kiplot/out_kibom.py new file mode 100644 index 00000000..dcf4c38a --- /dev/null +++ b/kiplot/out_kibom.py @@ -0,0 +1,51 @@ +import os +from glob import (glob) +from subprocess import (check_output, STDOUT, CalledProcessError) +from .out_base import (BaseOutput) +from .error import KiPlotConfigurationError +from .misc import (CMD_KIBOM, URL_KIBOM, BOM_ERROR) +from .kiplot import (GS, check_script) +from . import log + +logger = log.get_logger(__name__) + + +class KiBoM(BaseOutput): + def __init__(self, name, type, description): + super(KiBoM, self).__init__(name, type, description) + self._sch_related = True + # Options + self._format = 'HTML' + + @property + def format(self): + return self._format + + @format.setter + def format(self, val): + if val not in ['HTML', 'CSV']: + raise KiPlotConfigurationError("`format` must be either `HTML` or `CSV`") + self._format = val + + def run(self, output_dir, board): + check_script(CMD_KIBOM, URL_KIBOM) + format = self.format.lower() + prj = os.path.splitext(os.path.relpath(GS.pcb_file))[0] + logger.debug('Doing BoM, format '+format+' prj: '+prj) + cmd = [CMD_KIBOM, prj+'.xml', os.path.join(output_dir, os.path.basename(prj))+'.'+format] + logger.debug('Running: '+str(cmd)) + try: + cmd_output = check_output(cmd, stderr=STDOUT) + except CalledProcessError as e: + logger.error('Failed to create BoM, error %d', e.returncode) + if e.output: + logger.debug('Output from command: '+e.output.decode()) + exit(BOM_ERROR) + prj = os.path.basename(prj) + for f in glob(os.path.join(output_dir, prj)+'*.tmp'): + os.remove(f) + logger.debug('Output from command:\n'+cmd_output.decode()) + + +# Register it +BaseOutput.register('kibom', KiBoM) diff --git a/kiplot/out_pdf.py b/kiplot/out_pdf.py new file mode 100644 index 00000000..3cd953c5 --- /dev/null +++ b/kiplot/out_pdf.py @@ -0,0 +1,33 @@ +from pcbnew import PLOT_FORMAT_PDF +from .out_base import BaseOutput +from .out_any_layer import AnyLayer +from .error import KiPlotConfigurationError + + +class PDF(AnyLayer): + def __init__(self, name, type, description): + super(PDF, self).__init__(name, type, description) + self._plot_format = PLOT_FORMAT_PDF + # Options + self.line_width = 0.1 + self.mirror_plot = False + self.negative_plot = False + self._drill_marks = 'full' + + @property + def drill_marks(self): + return self._drill_marks + + @drill_marks.setter + def drill_marks(self, val): + if val not in self._drill_marks_map: + raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val)) + self._drill_marks = val + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + self._drill_marks = self._drill_marks_map[self._drill_marks] + + +# Register it +BaseOutput.register('pdf', PDF) diff --git a/kiplot/out_pdf_pcb_print.py b/kiplot/out_pdf_pcb_print.py new file mode 100644 index 00000000..d2139302 --- /dev/null +++ b/kiplot/out_pdf_pcb_print.py @@ -0,0 +1,51 @@ +from subprocess import (call) +from .out_base import BaseOutput +from .pre_base import BasePreFlight +from .error import (KiPlotConfigurationError, PlotError) +from .kiplot import (check_script, GS) +from .misc import (CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT) +from . import log + +logger = log.get_logger(__name__) + + +class PDFPcbPrint(BaseOutput): + def __init__(self, name, type, description): + super(PDFPcbPrint, self).__init__(name, type, description) + # Options + self.output_name = 'pdf_pcb_print.pdf' + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + # We need layers + if not self._layers: + raise KiPlotConfigurationError("Missing `layers` list") + + def run(self, output_dir, board): + check_script(CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, '1.4.1') + # Verify the inner layers + layer_cnt = board.GetCopperLayerCount() + for l in self._layers: + # for inner layers, we can now check if the layer exists + if l.is_inner: + if l.id < 1 or l.id >= layer_cnt - 1: + raise PlotError("Inner layer `{}` is not valid for this board".format(l)) + cmd = [CMD_PCBNEW_PRINT_LAYERS, 'export', '--output_name', self.output_name] + if BasePreFlight.get_option('check_zone_fills'): + cmd.append('-f') + cmd.extend([GS.pcb_file, output_dir]) + if GS.debug_enabled: + cmd.insert(1, '-vv') + cmd.insert(1, '-r') + # Add the layers + for l in self._layers: + cmd.append(l.name) + logger.debug('Executing: '+str(cmd)) + ret = call(cmd) + if ret: + logger.error(CMD_PCBNEW_PRINT_LAYERS+' returned %d', ret) + exit(PDF_PCB_PRINT) + + +# Register it +BaseOutput.register('pdf_pcb_print', PDFPcbPrint) diff --git a/kiplot/out_pdf_sch_print.py b/kiplot/out_pdf_sch_print.py new file mode 100644 index 00000000..42eed3f3 --- /dev/null +++ b/kiplot/out_pdf_sch_print.py @@ -0,0 +1,37 @@ +import os +from subprocess import (call) +from .out_base import BaseOutput +from .kiplot import (check_eeschema_do, GS) +from .misc import (CMD_EESCHEMA_DO, PDF_SCH_PRINT) +from . import log + +logger = log.get_logger(__name__) + + +class PDFSchPrint(BaseOutput): + def __init__(self, name, type, description): + super(PDFSchPrint, self).__init__(name, type, description) + self._sch_related = True + # Options + self.output = '' + + def run(self, output_dir, board): + check_eeschema_do() + cmd = [CMD_EESCHEMA_DO, 'export', '--all_pages', '--file_format', 'pdf', GS.sch_file, output_dir] + if GS.debug_enabled: + cmd.insert(1, '-vv') + cmd.insert(1, '-r') + logger.debug('Executing: '+str(cmd)) + ret = call(cmd) + if ret: + logger.error(CMD_EESCHEMA_DO+' returned %d', ret) + exit(PDF_SCH_PRINT) + if self.output: + cur = os.path.abspath(os.path.join(output_dir, os.path.splitext(os.path.basename(GS.pcb_file))[0]) + '.pdf') + new = os.path.abspath(os.path.join(output_dir, self.output)) + logger.debug('Moving '+cur+' -> '+new) + os.rename(cur, new) + + +# Register it +BaseOutput.register('pdf_sch_print', PDFSchPrint) diff --git a/kiplot/out_position.py b/kiplot/out_position.py new file mode 100644 index 00000000..842d35d7 --- /dev/null +++ b/kiplot/out_position.py @@ -0,0 +1,171 @@ +import os +import operator +from datetime import datetime +from pcbnew import (IU_PER_MM, IU_PER_MILS) +from .out_base import BaseOutput +from .error import KiPlotConfigurationError + + +class Position(BaseOutput): + def __init__(self, name, type, description): + super(Position, self).__init__(name, type, description) + # Options + self._format = 'ASCII' + self.separate_files_for_front_and_back = True + self.only_smd = True + self._units = 'millimeters' + + @property + def format(self): + return self._format + + @format.setter + def format(self, val): + if val not in ['ASCII', 'CSV']: + raise KiPlotConfigurationError("`format` must be either `ASCII` or `CSV`") + self._format = val + + @property + def units(self): + return self._units + + @units.setter + def units(self, val): + if val not in ['millimeters', 'inches']: + raise KiPlotConfigurationError("`units` must be either `millimeters` or `inches`") + self._units = val + + def _do_position_plot_ascii(self, board, output_dir, columns, modulesStr, maxSizes): + name = os.path.splitext(os.path.basename(board.GetFileName()))[0] + topf = None + botf = None + bothf = None + if self.separate_files_for_front_and_back: + topf = open(os.path.join(output_dir, "{}-top.pos".format(name)), 'w') + botf = open(os.path.join(output_dir, "{}-bottom.pos".format(name)), 'w') + else: + bothf = open(os.path.join(output_dir, "{}-both.pos").format(name), 'w') + + files = [f for f in [topf, botf, bothf] if f is not None] + for f in files: + f.write('### Module positions - created on {} ###\n'.format(datetime.now().strftime("%a %d %b %Y %X %Z"))) + f.write('### Printed by KiPlot\n') + unit = {'millimeters': 'mm', 'inches': 'in'}[self.units] + f.write('## Unit = {}, Angle = deg.\n'.format(unit)) + + if topf is not None: + topf.write('## Side : top\n') + if botf is not None: + botf.write('## Side : bottom\n') + if bothf is not None: + bothf.write('## Side : both\n') + + for f in files: + f.write('# ') + for idx, col in enumerate(columns): + if idx > 0: + f.write(" ") + f.write("{0: <{width}}".format(col, width=maxSizes[idx])) + f.write('\n') + + # Account for the "# " at the start of the comment column + maxSizes[0] = maxSizes[0] + 2 + + for m in modulesStr: + fle = bothf + if fle is None: + if m[-1] == "top": + fle = topf + else: + fle = botf + for idx, col in enumerate(m): + if idx > 0: + fle.write(" ") + fle.write("{0: <{width}}".format(col, width=maxSizes[idx])) + fle.write("\n") + + for f in files: + f.write("## End\n") + + if topf is not None: + topf.close() + if botf is not None: + botf.close() + if bothf is not None: + bothf.close() + + def _do_position_plot_csv(self, board, output_dir, columns, modulesStr): + name = os.path.splitext(os.path.basename(board.GetFileName()))[0] + topf = None + botf = None + bothf = None + if self.separate_files_for_front_and_back: + topf = open(os.path.join(output_dir, "{}-top-pos.csv".format(name)), 'w') + botf = open(os.path.join(output_dir, "{}-bottom-pos.csv".format(name)), 'w') + else: + bothf = open(os.path.join(output_dir, "{}-both-pos.csv").format(name), 'w') + + files = [f for f in [topf, botf, bothf] if f is not None] + + for f in files: + f.write(",".join(columns)) + f.write("\n") + + for m in modulesStr: + fle = bothf + if fle is None: + if m[-1] == "top": + fle = topf + else: + fle = botf + fle.write(",".join('"{}"'.format(e) for e in m)) + fle.write("\n") + + if topf is not None: + topf.close() + if botf is not None: + botf.close() + if bothf is not None: + bothf.close() + + def run(self, output_dir, board): + columns = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"] + colcount = len(columns) + # Note: the parser already checked the units are milimeters or inches + conv = 1.0 + if self.units == 'millimeters': + conv = 1.0 / IU_PER_MM + else: # self.units == 'inches': + conv = 0.001 / IU_PER_MILS + # Format all strings + modules = [] + for m in sorted(board.GetModules(), key=operator.methodcaller('GetReference')): + if (self.only_smd and m.GetAttributes() == 1) or not self.only_smd: + center = m.GetCenter() + # See PLACE_FILE_EXPORTER::GenPositionData() in + # export_footprints_placefile.cpp for C++ version of this. + modules.append([ + "{}".format(m.GetReference()), + "{}".format(m.GetValue()), + "{}".format(m.GetFPID().GetLibItemName()), + "{:.4f}".format(center.x * conv), + "{:.4f}".format(-center.y * conv), + "{:.4f}".format(m.GetOrientationDegrees()), + "{}".format("bottom" if m.IsFlipped() else "top") + ]) + + # Find max width for all columns + maxlengths = [0] * colcount + for row in range(len(modules)): + for col in range(colcount): + maxlengths[col] = max(maxlengths[col], len(modules[row][col])) + + # Note: the parser already checked the format is ASCII or CSV + if self.format == 'ASCII': + self._do_position_plot_ascii(board, output_dir, columns, modules, maxlengths) + else: # if self.format == 'CSV': + self._do_position_plot_csv(board, output_dir, columns, modules) + + +# Register it +BaseOutput.register('position', Position) diff --git a/kiplot/out_ps.py b/kiplot/out_ps.py new file mode 100644 index 00000000..50e03ded --- /dev/null +++ b/kiplot/out_ps.py @@ -0,0 +1,46 @@ +from pcbnew import PLOT_FORMAT_POST +from .out_base import BaseOutput +from .out_any_layer import AnyLayer +from .error import KiPlotConfigurationError + + +class PS(AnyLayer): + def __init__(self, name, type, description): + super(PS, self).__init__(name, type, description) + self._plot_format = PLOT_FORMAT_POST + # Options + self.line_width = 0.15 + self.mirror_plot = False + self.negative_plot = False + self.sketch_plot = True + self.scaling = 2 + self._drill_marks = 'full' + self.scale_adjust_x = 1.0 + self.scale_adjust_y = 1.0 + self.width_adjust = 0 + self.a4_output = True + + @property + def drill_marks(self): + return self._drill_marks + + @drill_marks.setter + def drill_marks(self, val): + if val not in self._drill_marks_map: + raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val)) + self._drill_marks = val + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + self._drill_marks = self._drill_marks_map[self._drill_marks] + + def _configure_plot_ctrl(self, po, output_dir): + super()._configure_plot_ctrl(po, output_dir) + po.SetWidthAdjust(self.width_adjust) + po.SetFineScaleAdjustX(self.scale_adjust_x) + po.SetFineScaleAdjustX(self.scale_adjust_y) + po.SetA4Output(self.a4_output) + + +# Register it +BaseOutput.register('ps', PS) diff --git a/kiplot/out_step.py b/kiplot/out_step.py new file mode 100644 index 00000000..b52cc56c --- /dev/null +++ b/kiplot/out_step.py @@ -0,0 +1,74 @@ +import os +import re +from subprocess import (check_output, STDOUT, CalledProcessError) +from .out_base import BaseOutput +from .error import KiPlotConfigurationError +from .misc import (KICAD2STEP, KICAD2STEP_ERR) +from .kiplot import (GS) +from . import log + +logger = log.get_logger(__name__) + + +class STEP(BaseOutput): + def __init__(self, name, type, description): + super(STEP, self).__init__(name, type, description) + # Options + self.metric_units = True + self._origin = 'grid' + self.no_virtual = False # exclude 3D models for components with 'virtual' attribute + self.min_distance = -1 # Minimum distance between points to treat them as separate ones (default 0.01 mm) + self.output = '' + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, val): + if (val not in ['grid', 'drill']) and (re.match(r'[-\d\.]+\s*,\s*[-\d\.]+\s*$', val) is None): + raise KiPlotConfigurationError('Origin must be `grid` or `drill` or `X,Y`') + self._origin = val + + def run(self, output_dir, board): + # Output file name + output = self.output + if not output: + output = os.path.splitext(os.path.basename(GS.pcb_file))[0]+'.step' + output = os.path.abspath(os.path.join(output_dir, output)) + # Make units explicit + if self.metric_units: + units = 'mm' + else: + units = 'in' + # Base command with overwrite + cmd = [KICAD2STEP, '-o', output, '-f'] + # Add user options + if self.no_virtual: + cmd.append('--no-virtual') + if self.min_distance >= 0: + cmd.extend(['--min-distance', "{}{}".format(self.min_distance, units)]) + if self.origin == 'drill': + cmd.append('--drill-origin') + elif self.origin == 'grid': + cmd.append('--grid-origin') + else: + cmd.extend(['--user-origin', "{}{}".format(self.origin.replace(',', 'x'), units)]) + # The board + cmd.append(GS.pcb_file) + # Execute and inform is successful + logger.debug('Executing: '+str(cmd)) + try: + cmd_output = check_output(cmd, stderr=STDOUT) + except CalledProcessError as e: # pragma: no cover + # Current kicad2step always returns 0!!!! + # This is why I'm excluding it from coverage + logger.error('Failed to create Step file, error %d', e.returncode) + if e.output: + logger.debug('Output from command: '+e.output.decode()) + exit(KICAD2STEP_ERR) + logger.debug('Output from command:\n'+cmd_output.decode()) + + +# Register it +BaseOutput.register('step', STEP) diff --git a/kiplot/out_svg.py b/kiplot/out_svg.py new file mode 100644 index 00000000..746667d0 --- /dev/null +++ b/kiplot/out_svg.py @@ -0,0 +1,33 @@ +from pcbnew import PLOT_FORMAT_SVG +from .out_base import BaseOutput +from .out_any_layer import AnyLayer +from .error import KiPlotConfigurationError + + +class SVG(AnyLayer): + def __init__(self, name, type, description): + super(SVG, self).__init__(name, type, description) + self._plot_format = PLOT_FORMAT_SVG + # Options + self.line_width = 0.25 + self.mirror_plot = False + self.negative_plot = False + self._drill_marks = 'full' + + @property + def drill_marks(self): + return self._drill_marks + + @drill_marks.setter + def drill_marks(self, val): + if val not in self._drill_marks_map: + raise KiPlotConfigurationError("Unknown drill mark type: {}".format(val)) + self._drill_marks = val + + def config(self, outdir, options, layers): + super().config(outdir, options, layers) + self._drill_marks = self._drill_marks_map[self._drill_marks] + + +# Register it +BaseOutput.register('svg', SVG) diff --git a/kiplot/plot_config.py b/kiplot/plot_config.py deleted file mode 100644 index b4264278..00000000 --- a/kiplot/plot_config.py +++ /dev/null @@ -1,555 +0,0 @@ -import pcbnew -import re - -from . import error -from . import log - -logger = log.get_logger(__name__) - - -class KiPlotConfigurationError(error.KiPlotError): - pass - - -class TypeOptions(object): - - def validate(self): - """ - Return list of invalid settings - """ - - return [] - - -class LayerOptions(TypeOptions): - """ - Common options that all layer outputs have - """ - - AUTO_SCALE = 0 - - def __init__(self): - - super(LayerOptions, self).__init__() - - self.exclude_edge_layer = False - self.exclude_pads_from_silkscreen = False - self.plot_sheet_reference = False - - self._supports_line_width = False - self._line_width = 0 - - self._supports_aux_axis_origin = False - self._use_aux_axis_as_origin = False - - # override for scalable formats - self._supports_scaling = False - - self._auto_scale = False - self._scaling = 1 - - self._supports_mirror = False - self._mirror_plot = False - - self._supports_negative = False - self._negative_plot = False - - self._supports_drill_marks = False - self._drill_marks = pcbnew.PCB_PLOT_PARAMS.NO_DRILL_SHAPE - - self._support_sketch_mode = False - self._sketch_mode = False - - @property - def line_width(self): - return self._line_width - - @line_width.setter - def line_width(self, value): - """ - Set the line width, in mm - """ - if self._supports_line_width: - self._line_width = pcbnew.FromMM(value) - else: - raise KiPlotConfigurationError( - "This output doesn't support setting line width") - - @property - def auto_scale(self): - return self._auto_scale - - @property - def scaling(self): - return self._scaling - - @scaling.setter - def scaling(self, val): - """ - Set scaling, if possible. AUTO_SCALE to set auto scaling - """ - - if self._supports_scaling: - - if val == self.AUTO_SCALE: - self._scaling = 1 - self._auto_scale = True - else: - self._scaling = val - self._auto_scale = False - else: - raise KiPlotConfigurationError( - "This Layer output does not support scaling") - - @property - def mirror_plot(self): - return self._mirror_plot - - @mirror_plot.setter - def mirror_plot(self, val): - - if self._supports_mirror: - self._mirror_plot = val - else: - raise KiPlotConfigurationError( - "This Layer output does not support mirror plotting") - - @property - def negative_plot(self): - return self._negative_plot - - @negative_plot.setter - def negative_plot(self, val): - - if self._supports_mirror: - self._negative_plot = val - else: - raise KiPlotConfigurationError( - "This Layer output does not support negative plotting") - - @property - def drill_marks(self): - return self._drill_marks - - @drill_marks.setter - def drill_marks(self, val): - - if self._supports_drill_marks: - - try: - drill_mark = { - 'none': pcbnew.PCB_PLOT_PARAMS.NO_DRILL_SHAPE, - 'small': pcbnew.PCB_PLOT_PARAMS.SMALL_DRILL_SHAPE, - 'full': pcbnew.PCB_PLOT_PARAMS.FULL_DRILL_SHAPE, - }[val] - except KeyError: - raise KiPlotConfigurationError( - "Unknown drill mark type: {}".format(val)) - - self._drill_marks = drill_mark - else: - raise KiPlotConfigurationError( - "This Layer output does not support drill marks") - - @property - def use_aux_axis_as_origin(self): - return self._use_aux_axis_as_origin - - @use_aux_axis_as_origin.setter - def use_aux_axis_as_origin(self, val): - - if self._supports_aux_axis_origin: - self._use_aux_axis_as_origin = val - else: - raise KiPlotConfigurationError( - "This Layer output does not support using the auxiliary" - " axis as the origin") - - @property - def sketch_mode(self): - return self._sketch_mode - - @sketch_mode.setter - def sketch_mode(self, val): - - if self._supports_sketch_mode: - self._sketch_mode = val - else: - raise KiPlotConfigurationError( - "This Layer output does not support sketch mode") - - -class GerberOptions(LayerOptions): - - def __init__(self): - - super(GerberOptions, self).__init__() - - self._supports_line_width = True - self._supports_aux_axis_origin = True - - self.subtract_mask_from_silk = False - self.use_protel_extensions = False - self.create_gerber_job_file = False - self.use_gerber_x2_attributes = False - self.use_gerber_net_attributes = False - - # either 5 or 6 - self._gerber_precision = None - - def validate(self): - - errs = super(GerberOptions, self).validate() - - if (not self.use_gerber_x2_attributes and - self.use_gerber_net_attributes): - errs.append("Must set Gerber X2 attributes to use net attributes") - - return errs - - @property - def gerber_precision(self): - return self._gerber_precision - - @gerber_precision.setter - def gerber_precision(self, val): - """ - Set gerber precision: either 4.5 or 4.6 - """ - if val == 4.5: - self._gerber_precision = 5 - elif val == 4.6: - self._gerber_precision = 6 - else: - raise KiPlotConfigurationError( - "Bad Gerber precision : {}".format(val)) - - -class HpglOptions(LayerOptions): - - def __init__(self): - - super(HpglOptions, self).__init__() - - self._supports_sketch_mode = True - self._supports_mirror = True - self._supports_scaling = True - self._supports_drill_marks = True - - self._pen_width = None - - @property - def pen_width(self): - return self._pen_width - - @pen_width.setter - def pen_width(self, pw_mm): - self._pen_width = pcbnew.FromMM(pw_mm) - - -class PsOptions(LayerOptions): - - def __init__(self): - - super(PsOptions, self).__init__() - - self._supports_mirror = True - self._supports_negative = True - self._supports_scaling = True - self._supports_drill_marks = True - self._supports_line_width = True - self._supports_sketch_mode = True - - self.scale_adjust_x = 1.0 - self.scale_adjust_y = 1.0 - - self._width_adjust = 0 - - self.a4_output = False - - @property - def width_adjust(self): - return self._width_adjust - - @width_adjust.setter - def width_adjust(self, width_adjust_mm): - self._width_adjust = pcbnew.FromMM(width_adjust_mm) - - -class SvgOptions(LayerOptions): - - def __init__(self): - - super(SvgOptions, self).__init__() - - self._supports_line_width = True - self._supports_mirror = True - self._supports_negative = True - self._supports_drill_marks = True - - -class PdfOptions(LayerOptions): - - def __init__(self): - - super(PdfOptions, self).__init__() - - self._supports_line_width = True - self._supports_mirror = True - self._supports_negative = True - self._supports_drill_marks = True - - -class DxfOptions(LayerOptions): - - def __init__(self): - - super(DxfOptions, self).__init__() - - self._supports_aux_axis_origin = True - self._supports_drill_marks = True - - self.polygon_mode = False - - -class DrillOptions(TypeOptions): - - def __init__(self): - - super(DrillOptions, self).__init__() - - self.use_aux_axis_as_origin = False - - self.map_options = None - self.report_options = None - - @property - def generate_map(self): - return self.map_options is not None - - @property - def generate_report(self): - return self.report_options is not None - - -class ExcellonOptions(DrillOptions): - - def __init__(self): - - super(ExcellonOptions, self).__init__() - - self.metric_units = True - self.minimal_header = False - self.mirror_y_axis = False - - -class GerberDrillOptions(DrillOptions): - - def __init__(self): - - super(GerberDrillOptions, self).__init__() - - -class DrillReportOptions(object): - - def __init__(self): - self.filename = None - - -class DrillMapOptions(object): - - def __init__(self): - self.type = None - - -class PositionOptions(TypeOptions): - - def __init__(self): - self.format = None - self.units = None - self.separate_files_for_front_and_back = None - - def validate(self): - errs = [] - if self.format not in ["ASCII", "CSV"]: - errs.append("Format must be either ASCII or CSV") - if self.units not in ["millimeters", "inches"]: - errs.append("Units must be either millimeters or inches") - return errs - - -class StepOptions(TypeOptions): - - def __init__(self): - self.metric_units = True - self.origin = None - self.min_distance = None - self.no_virtual = False - self.output = None - - def validate(self): - errs = [] - # origin (required) - if (self.origin not in ['grid', 'drill']) and (re.match(r'[-\d\.]+\s*,\s*[-\d\.]+\s*$', self.origin) is None): - errs.append('Origin must be "grid" or "drill" or "X,Y"') - # min_distance (not required) - if (self.min_distance is not None) and (not isinstance(self.min_distance, (int, float))): - errs.append('min_distance must be a number') - return errs - - -class KiBoMOptions(TypeOptions): - - def __init__(self): - self.format = None - - def validate(self): - errs = [] - if self.format not in ["HTML", "CSV"]: - errs.append("Format must be either HTML or CSV") - return errs - - -class IBoMOptions(TypeOptions): - - def __init__(self): - self.blacklist = None - self.name_format = None - - -class SchPrintOptions(TypeOptions): - - def __init__(self): - self.output = None - - -class PcbPrintOptions(TypeOptions): - - def __init__(self): - self.output = None - - -class OutputOptions(object): - - GERBER = 'gerber' - POSTSCRIPT = 'ps' - HPGL = 'hpgl' - SVG = 'svg' - PDF = 'pdf' - DXF = 'dxf' - - EXCELLON = 'excellon' - GERB_DRILL = 'gerb_drill' - POSITION = 'position' - KIBOM = 'kibom' - IBOM = 'ibom' - PDF_SCH_PRINT = 'pdf_sch_print' - PDF_PCB_PRINT = 'pdf_pcb_print' - STEP = 'step' - - def __init__(self, otype): - self.type = otype - - if otype == self.GERBER: - self.type_options = GerberOptions() - elif otype == self.POSTSCRIPT: - self.type_options = PsOptions() - elif otype == self.HPGL: - self.type_options = HpglOptions() - elif otype == self.SVG: - self.type_options = SvgOptions() - elif otype == self.DXF: - self.type_options = DxfOptions() - elif otype == self.PDF: - self.type_options = PdfOptions() - elif otype == self.EXCELLON: - self.type_options = ExcellonOptions() - elif otype == self.GERB_DRILL: - self.type_options = GerberDrillOptions() - elif otype == self.POSITION: - self.type_options = PositionOptions() - elif otype == self.KIBOM: - self.type_options = KiBoMOptions() - elif otype == self.IBOM: - self.type_options = IBoMOptions() - elif otype == self.PDF_SCH_PRINT: - self.type_options = SchPrintOptions() - elif otype == self.PDF_PCB_PRINT: - self.type_options = PcbPrintOptions() - elif otype == self.STEP: - self.type_options = StepOptions() - else: # pragma: no cover - # If we get here it means the above if is incomplete - raise KiPlotConfigurationError("Output options not implemented for "+otype) - - def validate(self): - return self.type_options.validate() - - -class LayerInfo(object): - - def __init__(self, layer, is_inner, name): - - self.layer = layer - self.is_inner = is_inner - self.name = name - - -class LayerConfig(object): - - def __init__(self, layer): - - # the Pcbnew layer - self.layer = layer - self.suffix = "" - self.desc = "desc" - - -class PlotOutput(object): - - def __init__(self, name, description, otype, options): - self.name = name - self.description = description - self.outdir = None - self.options = options - - self.layers = [] - - def validate(self): - return self.options.validate() - - -class PlotConfig(object): - - def __init__(self): - - self._outputs = [] - self.outdir = None - - self.check_zone_fills = False - self.run_drc = False - self.update_xml = False - self.ignore_unconnected = False - self.run_erc = False - self.filters = None - - def add_output(self, new_op): - self._outputs.append(new_op) - - def add_filter(self, comment, number, regex): - logger.debug("Adding DRC/ERC filter '{}','{}','{}'".format(comment, number, regex)) - if self.filters is None: - self.filters = '' - if comment: - self.filters += '# '+comment+'\n' - self.filters += '{},{}\n'.format(number, regex) - - def validate(self): - errs = [] - for o in self._outputs: - errs += o.validate() - return errs - - @property - def outputs(self): - return self._outputs diff --git a/kiplot/pre_base.py b/kiplot/pre_base.py new file mode 100644 index 00000000..d3489ff1 --- /dev/null +++ b/kiplot/pre_base.py @@ -0,0 +1,82 @@ +from . import log + +logger = log.get_logger(__name__) + + +class BasePreFlight(object): + _registered = {} + _in_use = {} + _options = {} + + def __init__(self, name, value): + self._value = value + self._name = name + self._sch_related = False + self._pcb_related = False + self._enabled = True + + @staticmethod + def register(name, aclass): + BasePreFlight._registered[name] = aclass + + @staticmethod + def is_registered(name): + return name in BasePreFlight._registered + + @staticmethod + def get_class_for(name): + return BasePreFlight._registered[name] + + @staticmethod + def add_preflight(o_pre): + BasePreFlight._in_use[o_pre._name] = o_pre + + @staticmethod + def get_preflight(name): + return BasePreFlight._in_use.get(name) + + @staticmethod + def get_in_use_objs(): + return BasePreFlight._in_use.values() + + @staticmethod + def _set_option(name, value): + BasePreFlight._options[name] = value + + @staticmethod + def get_option(name): + return BasePreFlight._options.get(name) + + @staticmethod + def run_enabled(): + for k, v in BasePreFlight._in_use.items(): + if v._enabled: + logger.debug('Preflight apply '+k) + v.apply() + for k, v in BasePreFlight._in_use.items(): + if v._enabled: + logger.debug('Preflight run '+k) + v.run() + + def disable(self): + self._enabled = False + + def is_enabled(self): + return self._enabled + + def __str__(self): + return "{}: {}".format(self._name, self._enabled) + + def is_sch(self): + """ True for outputs that works on the schematic """ + return self._sch_related + + def is_pcb(self): + """ True for outputs that works on the PCB """ + return self._pcb_related + + def run(self, brd_file): # pragma: no cover + logger.error("The run method for the preflight class name `{}` isn't implemented".format(self._name)) + + def apply(self, brd_file): # pragma: no cover + logger.error("The apply method for the preflight class name `{}` isn't implemented".format(self._name)) diff --git a/kiplot/pre_check_zone_fills.py b/kiplot/pre_check_zone_fills.py new file mode 100644 index 00000000..a6ee9733 --- /dev/null +++ b/kiplot/pre_check_zone_fills.py @@ -0,0 +1,21 @@ +from .pre_base import (BasePreFlight) +from .error import (KiPlotConfigurationError) + + +class CheckZoneFills(BasePreFlight): + def __init__(self, name, value): + super().__init__(name, value) + if not isinstance(value, bool): + raise KiPlotConfigurationError('must be boolean') + self._enabled = value + self._pcb_related = True + + def run(self): + pass + + def apply(self): + BasePreFlight._set_option('check_zone_fills', self._enabled) + + +# Register it +BasePreFlight.register('check_zone_fills', CheckZoneFills) diff --git a/kiplot/pre_drc.py b/kiplot/pre_drc.py new file mode 100644 index 00000000..47bc3487 --- /dev/null +++ b/kiplot/pre_drc.py @@ -0,0 +1,47 @@ +from sys import (exit) +from subprocess import (call) +from .pre_base import (BasePreFlight) +from .error import (KiPlotConfigurationError) +from .kiplot import (GS, check_script) +from .misc import (CMD_PCBNEW_RUN_DRC, URL_PCBNEW_RUN_DRC, DRC_ERROR) +from .log import (get_logger) + +logger = get_logger(__name__) + + +class DRC(BasePreFlight): + def __init__(self, name, value): + super().__init__(name, value) + if not isinstance(value, bool): + raise KiPlotConfigurationError('must be boolean') + self._enabled = value + self._pcb_related = True + + def run(self): + check_script(CMD_PCBNEW_RUN_DRC, URL_PCBNEW_RUN_DRC, '1.4.0') + cmd = [CMD_PCBNEW_RUN_DRC, 'run_drc'] + if GS.filter_file: + cmd.extend(['-f', GS.filter_file]) + cmd.extend([GS.pcb_file, GS.out_dir]) + # If we are in verbose mode enable debug in the child + if GS.debug_enabled: + cmd.insert(1, '-vv') + cmd.insert(1, '-r') + if BasePreFlight.get_option('ignore_unconnected'): + cmd.insert(1, '-i') + logger.info('- Running the DRC') + logger.debug('Executing: '+str(cmd)) + ret = call(cmd) + if ret: + if ret < 0: + logger.error('DRC errors: %d', -ret) + else: + logger.error('DRC returned %d', ret) + exit(DRC_ERROR) + + def apply(self): + pass + + +# Register it +BasePreFlight.register('run_drc', DRC) diff --git a/kiplot/pre_erc.py b/kiplot/pre_erc.py new file mode 100644 index 00000000..3f63af46 --- /dev/null +++ b/kiplot/pre_erc.py @@ -0,0 +1,45 @@ +from sys import (exit) +from subprocess import (call) +from .pre_base import (BasePreFlight) +from .kiplot import (GS, check_eeschema_do) +from .error import (KiPlotConfigurationError) +from .misc import (CMD_EESCHEMA_DO, ERC_ERROR) +from .log import (get_logger) + +logger = get_logger(__name__) + + +class ERC(BasePreFlight): + def __init__(self, name, value): + super().__init__(name, value) + if not isinstance(value, bool): + raise KiPlotConfigurationError('must be boolean') + self._enabled = value + self._sch_related = True + + def run(self): + check_eeschema_do() + cmd = [CMD_EESCHEMA_DO, 'run_erc'] + if GS.filter_file: + cmd.extend(['-f', GS.filter_file]) + cmd.extend([GS.sch_file, GS.out_dir]) + # If we are in verbose mode enable debug in the child + if GS.debug_enabled: + cmd.insert(1, '-vv') + cmd.insert(1, '-r') + logger.info('- Running the ERC') + logger.debug('Executing: '+str(cmd)) + ret = call(cmd) + if ret: + if ret < 0: + logger.error('ERC errors: %d', -ret) + else: + logger.error('ERC returned %d', ret) + exit(ERC_ERROR) + + def apply(self): + pass + + +# Register it +BasePreFlight.register('run_erc', ERC) diff --git a/kiplot/pre_filters.py b/kiplot/pre_filters.py new file mode 100644 index 00000000..ef2d5a7a --- /dev/null +++ b/kiplot/pre_filters.py @@ -0,0 +1,22 @@ +import os +from .kiplot import (GS) +from .pre_base import (BasePreFlight) + + +class Filters(BasePreFlight): + def __init__(self, name, value): + super().__init__(name, value) + + def run(self): + pass + + def apply(self): + # Create the filters file + if self._value: + GS.filter_file = os.path.join(GS.out_dir, 'kiplot_errors.filter') + with open(GS.filter_file, 'w') as f: + f.write(self._value) + + +# Register it +BasePreFlight.register('filters', Filters) diff --git a/kiplot/pre_ignore_unconnected.py b/kiplot/pre_ignore_unconnected.py new file mode 100644 index 00000000..3b4a08d7 --- /dev/null +++ b/kiplot/pre_ignore_unconnected.py @@ -0,0 +1,21 @@ +from .pre_base import (BasePreFlight) +from .error import (KiPlotConfigurationError) + + +class IgnoreUnconnected(BasePreFlight): + def __init__(self, name, value): + super().__init__(name, value) + if not isinstance(value, bool): + raise KiPlotConfigurationError('must be boolean') + self._enabled = value + self._pcb_related = True + + def run(self, brd_file): + pass + + def apply(self): + BasePreFlight._set_option('ignore_unconnected', self._enabled) + + +# Register it +BasePreFlight.register('ignore_unconnected', IgnoreUnconnected) diff --git a/kiplot/pre_update_xml.py b/kiplot/pre_update_xml.py new file mode 100644 index 00000000..d0831ef4 --- /dev/null +++ b/kiplot/pre_update_xml.py @@ -0,0 +1,39 @@ +from sys import (exit) +from subprocess import (call) +from .pre_base import (BasePreFlight) +from .error import (KiPlotConfigurationError) +from .kiplot import (GS, check_eeschema_do) +from .misc import (CMD_EESCHEMA_DO, BOM_ERROR) +from .log import (get_logger) + +logger = get_logger(__name__) + + +class UpdateXML(BasePreFlight): + def __init__(self, name, value): + super().__init__(name, value) + if not isinstance(value, bool): + raise KiPlotConfigurationError('must be boolean') + self._enabled = value + self._sch_related = True + + def run(self): + check_eeschema_do() + cmd = [CMD_EESCHEMA_DO, 'bom_xml', GS.sch_file, GS.out_dir] + # If we are in verbose mode enable debug in the child + if GS.debug_enabled: + cmd.insert(1, '-vv') + cmd.insert(1, '-r') + logger.info('- Updating BoM in XML format') + logger.debug('Executing: '+str(cmd)) + ret = call(cmd) + if ret: + logger.error('Failed to update the BoM, error %d', ret) + exit(BOM_ERROR) + + def apply(self): + pass + + +# Register it +BasePreFlight.register('update_xml', UpdateXML)