""" Main Kiplot code """ from datetime import datetime import os import sys import operator from shutil import which from subprocess import call import logging from . import plot_config as PCfg from . import error from . import log from . import misc logger = log.get_logger(__name__) try: import pcbnew from pcbnew import GERBER_JOBFILE_WRITER except ImportError: log.init(False,False) logger.error("Failed to import pcbnew Python module." " Is KiCad installed?" " Do you need to add it to PYTHONPATH?") import sys sys.exit(misc.NO_PCBNEW_MODULE) class PlotError(error.KiPlotError): pass class Plotter(object): """ Main Plotter class - this is what will perform the plotting """ def __init__(self, cfg): self.cfg = cfg def plot(self, brd_file, target, invert): logger.debug("Starting plot of board {}".format(brd_file)) board = pcbnew.LoadBoard(brd_file) logger.debug("Board loaded") self._preflight_checks(brd_file) n = len(target) if n == 0 and invert: # Skip all targets logger.debug('Skipping all outputs') return for op in self.cfg.outputs: if (n == 0) or ((op.name in target) ^ invert): logger.debug("Processing output: {}".format(op.name)) logger.info('- %s (%s) [%s]' % (op.description,op.name,op.options.type)) # fresh plot controller pc = pcbnew.PLOT_CONTROLLER(board) self._configure_output_dir(pc, op) if self._output_is_layer(op): self._do_layer_plot(board, pc, op, brd_file) elif self._output_is_drill(op): self._do_drill_plot(board, pc, op) elif self._output_is_position(op): self._do_position_plot(board, pc, op) else: raise PlotError("Don't know how to plot type {}" .format(op.options.type)) else: logger.debug('Skipping %s output', op.name) def _preflight_checks(self, brd_file): logger.debug("Preflight checks") if self.cfg.check_zone_fills: logger.error('check_zone_fills not yet supported') sys.exit(misc.USUPPORTED_OPTION) if self.cfg.run_drc: self._run_drc(brd_file, self.cfg.ignore_unconnected) def _run_drc(self, brd_file, ignore_unconnected): if which('pcbnew_run_drc') is None: logger.error('No `pcbnew_run_drc` command found.\n' 'Please install it, visit: https://github.com/INTI-CMNB/kicad-automation-scripts') sys.exit(misc.MISSING_TOOL) cmd = ['pcbnew_run_drc', brd_file, '.'] # If we are in verbose mode enable debug in the child if logger.getEffectiveLevel() <= logging.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) sys.exit(DRC_ERROR) def _output_is_layer(self, output): 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): 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 _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, plot_ctrl, output, file_name): # set up plot options for the whole output self._configure_plot_ctrl(plot_ctrl, output) 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_ctrl.OpenPlotfile(suffix, plot_format, desc) 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.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, plot_ctrl, output): to = output.options.type_options outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory() # dialog_gendrill.cpp:357 if to.use_aux_axis_as_origin: offset = board.GetAuxOrigin() 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: raise error.PlotError("Can't make a writer for type {}" .format(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 {}" .format(outdir)) if gen_map: drill_writer.SetMapFileFormat(to.map_options.type) logger.debug("Generating drill map type {} in {}" .format(to.map_options.type, outdir)) drill_writer.CreateDrillandMapFilesSet(outdir, gen_drill, gen_map) if gen_report: drill_report_file = os.path.join(outdir, to.report_options.filename) logger.debug("Generating drill report: {}" .format(drill_report_file)) drill_writer.GenDrillReportFile(drill_report_file) def _do_position_plot_ascii(self, board, plot_ctrl, output, columns, modulesStr, maxSizes): to = output.options.type_options outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory() if not os.path.exists(outdir): os.makedirs(outdir) 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(outdir, "{}-top.pos".format(name)), 'w') botf = open(os.path.join(outdir, "{}-bottom.pos".format(name)), 'w') else: bothf = open(os.path.join(outdir, "{}-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, plot_ctrl, output, columns, modulesStr): to = output.options.type_options outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory() if not os.path.exists(outdir): os.makedirs(outdir) 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(outdir, "{}-top-pos.csv".format(name)), 'w') botf = open(os.path.join(outdir, "{}-bottom-pos.csv".format(name)), 'w') else: bothf = open(os.path.join(outdir, "{}-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, plot_ctrl, output): to = output.options.type_options columns = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"] colcount = len(columns) conv = 1.0 if to.units == 'millimeters': conv = 1.0 / pcbnew.IU_PER_MM elif to.units == 'inches': conv = 0.001 / pcbnew.IU_PER_MILS else: raise PlotError('Invalid units: {}'.format(to.units)) # 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])) if to.format.lower() == 'ascii': self._do_position_plot_ascii(board, plot_ctrl, output, columns, modules, maxlengths) elif to.format.lower() == 'csv': self._do_position_plot_csv(board, plot_ctrl, output, columns, modules) else: raise PlotError("Format is invalid: {}".format(to.format)) 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 _configure_pdf_opts(self, po, output): assert(output.options.type == PCfg.OutputOptions.PDF) # pdf_opts = output.options.type_options def _configure_svg_opts(self, po, output): assert(output.options.type == PCfg.OutputOptions.SVG) # pdf_opts = output.options.type_options def _configure_position_opts(self, po, output): assert(output.options.type == PCfg.OutputOptions.POSITION) def _configure_output_dir(self, plot_ctrl, output): po = plot_ctrl.GetPlotOptions() # outdir is a combination of the config and output outdir = os.path.join(self.cfg.outdir, output.outdir) logger.debug("Output destination: {}".format(outdir)) po.SetOutputDirectory(outdir) def _configure_plot_ctrl(self, plot_ctrl, output): logger.debug("Configuring plot controller for output") po = plot_ctrl.GetPlotOptions() 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) 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.SVG: self._configure_svg_opts(po, output) elif output.options.type == PCfg.OutputOptions.PDF: self._configure_pdf_opts(po, output) elif output.options.type == PCfg.OutputOptions.HPGL: self._configure_hpgl_opts(po, output) elif output.options.type == PCfg.OutputOptions.POSITION: self._configure_position_opts(po, output) po.SetDrillMarksType(opts.drill_marks) # We'll come back to this on a per-layer basis po.SetSkipPlotNPTH_Pads(False)