KiBot/kiplot/kiplot.py

621 lines
21 KiB
Python

"""
Main Kiplot code
"""
from datetime import datetime
import os
from sys import exit
import operator
from shutil import which
from subprocess import call, run, PIPE
import logging
from distutils.version import StrictVersion
import re
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
exit(misc.NO_PCBNEW_MODULE)
def check_version(command, version):
cmd = [command, '--version']
logger.debug('Running: '+str(cmd))
result = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
z=re.match(command+' (\d+\.\d+\.\d+)', result.stdout)
if not z:
logger.error('Unable to determine '+command+' version:\n'+result.stdout)
exit(misc.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)
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, skip_pre):
logger.debug("Starting plot of board {}".format(brd_file))
board = pcbnew.LoadBoard(brd_file)
logger.debug("Board loaded")
self._preflight_checks(brd_file, skip_pre)
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, skip_pre):
logger.debug("Preflight checks")
if not skip_pre is 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 == 'run_erc':
self.cfg.run_erc = False
logger.debug('Skipping run_erc')
else:
logger.error('Unknown action to skip: '+skip)
exit(misc.EXIT_BAD_ARGS)
if self.cfg.run_erc:
self._run_erc(brd_file)
if self.cfg.run_drc:
self._run_drc(brd_file, self.cfg.ignore_unconnected, self.cfg.check_zone_fills)
def _run_erc(self, brd_file):
if which('eeschema_do') is None:
logger.error('No `eeschema_do` command found.\n'
'Please install it, visit: https://github.com/INTI-CMNB/kicad-automation-scripts')
exit(misc.MISSING_TOOL)
check_version('eeschema_do','1.1.1')
sch_file = os.path.splitext(brd_file)[0] + '.sch'
if not os.path.isfile(sch_file):
logger.error('Missing schematic file: ' + sch_file)
exit(misc.NO_SCH_FILE)
cmd = ['eeschema_do', 'run_erc', sch_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')
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 _run_drc(self, brd_file, ignore_unconnected, check_zone_fills):
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')
exit(misc.MISSING_TOOL)
check_version('pcbnew_run_drc','1.1.0')
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')
if check_zone_fills:
cmd.insert(1, '-s')
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):
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)