Getting there

This commit is contained in:
John Beard 2018-06-01 23:56:22 +01:00
parent 21e185c9d6
commit 5298dc2c72
5 changed files with 518 additions and 8 deletions

View File

@ -2,10 +2,35 @@
kiplot:
version: 1
options:
basedir: /tmp/kiplot_out
outputs:
- 'gerbers':
- name: 'gerbers'
comment: "Gerbers for the board house"
type: gerber
dir: gerberdir
options:
subtract_mask_from_silk: true
use_protel_extensions: false
line_width: 0.15
exclude_edge_layer: false
exclude_pads_from_silkscreen: false
use_aux_axis_as_origin: false
layers:
- F.Cu
- F.SilkS
- layer: F.Cu
suffix: F_Cu
- layer: F.SilkS
suffix: F_SilkS
# - layer: Inner.1
# suffix: Inner_1
- name: excellon_drill
comment: "Excellon drill files"
type: excellon
dir: gerberdir
options:
metric_units: true
pth_and_npth_single_file: true
use_aux_axis_as_origin: false

View File

@ -2,22 +2,45 @@
import argparse
import logging
import os
import sys
from . import kiplot
from . import config_reader
def main():
parser = argparse.ArgumentParser(description='Command-line Plotting for KiCad')
parser = argparse.ArgumentParser(
description='Command-line Plotting for KiCad')
parser.add_argument('-v', '--verbose', action='store_true',
help='show debugging information')
parser.add_argument('-b', '--board-file', required=True,
help='The PCB .kicad-pcb board file')
parser.add_argument('-c', '--plot-config', required=True,
help='The plotting config file to use')
args = parser.parse_args()
log_level = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(level=log_level)
if not os.path.isfile(args.board_file):
logging.error("Board file not found: {}".format(args.board_file))
if not os.path.isfile(args.plot_config):
logging.error("Plot config file not found: {}"
.format(args.plot_config))
cr = config_reader.CfgYamlReader()
with open(args.plot_config) as cf_file:
cfg = cr.read(cf_file)
plotter = kiplot.Plotter(cfg)
plotter.plot(args.board_file)
if __name__ == "__main__":
main()

View File

@ -2,10 +2,220 @@
Class to read KiPlot config files
"""
import logging
import yaml
import os
import re
import pcbnew
from . import plot_config as PC
class CfgReader(object):
def __init__(self):
pass
pass
class CfgYamlReader(CfgReader):
class YamlError(Exception):
pass
def __init__(self):
super(CfgYamlReader, self).__init__()
def _check_version(self, data):
try:
version = data['kiplot']['version']
except KeyError:
raise self.YamlError("YAML config needs kiplot.version.")
return None
if version != 1:
raise self.YamlError("Unknown KiPlot config version: {}"
.format(version))
return None
return version
def _get_required(self, data, key):
try:
val = data[key]
except KeyError:
raise self.YamlError("Value is needed for {}".format(key))
return val
def _parse_out_opts(self, otype, options):
po = PC.OutputOptions(otype)
# options that apply to the specific output type
to = po.type_options
# common options - layer outputs
if otype in ['gerber']:
to.line_width = self._get_required(options, 'line_width')
to.exclude_edge_layer = self._get_required(
options, 'exclude_edge_layer')
to.exclude_pads_from_silkscreen = self._get_required(
options, 'exclude_pads_from_silkscreen')
to.use_aux_axis_as_origin = self._get_required(
options, 'use_aux_axis_as_origin')
# common options - drill outputs
if otype in ['excellon']:
to.use_aux_axis_as_origin = self._get_required(
options, 'use_aux_axis_as_origin')
# set type-specific options
if otype == 'gerber':
to.subtract_mask_from_silk = self._get_required(
options, 'subtract_mask_from_silk')
to.use_protel_extensions = self._get_required(
options, 'use_protel_extensions')
if otype == 'excellon':
to.metric_units = self._get_required(
options, 'metric_units')
to.pth_and_npth_single_file = self._get_required(
options, 'pth_and_npth_single_file')
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,
'F.Adhes': pcbnew.F_Adhes,
'B.Adhes': pcbnew.B_Adhes,
'F.Paste': pcbnew.F_Paste,
'B.Paste': pcbnew.B_Paste,
'F.SilkS': pcbnew.F_SilkS,
'B.SilkS': pcbnew.B_SilkS,
'F.Mask': pcbnew.F_Mask,
'B.Mask': pcbnew.B_Mask,
'Dwgs.User': pcbnew.Dwgs_User,
'Cmts.User': pcbnew.Cmts_User,
'Eco1.User': pcbnew.Eco1_User,
'Eco2.User': pcbnew.Eco2_User,
'Edge.Cuts': pcbnew.Edge_Cuts,
'Margin': pcbnew.Margin,
'F.CrtYd': pcbnew.F_CrtYd,
'B.CrtYd': pcbnew.B_CrtYd,
'F.Fab': pcbnew.F_Fab,
'B.Fab': pcbnew.B_Fab,
}
layer = None
if s in D:
layer = PC.LayerInfo(D[s], False)
elif s.startswith("Inner"):
m = re.match(r"^Inner\.([0-9]+)$", s)
if not m:
raise self.YamlError("Malformed inner layer name: {}"
.format(s))
layer = PC.LayerInfo(int(m.group(1)), True)
else:
raise self.YamlError("Unknown layer name: {}".format(s))
return layer
def _parse_layer(self, l_obj):
l_str = self._get_required(l_obj, 'layer')
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
def _parse_output(self, o_obj):
try:
name = o_obj['name']
except KeyError:
raise self.YamlError("Output needs a name")
try:
desc = o_obj['description']
except KeyError:
desc = None
try:
otype = o_obj['type']
except KeyError:
raise self.YamlError("Output needs a type")
if otype not in ['gerber', 'excellon']:
raise self.YamlError("Unknown output type: {}".format(otype))
try:
options = o_obj['options']
except KeyError:
raise self.YamlError("Output need to have options specified")
outdir = self._get_required(o_obj, 'dir')
output_opts = self._parse_out_opts(otype, options)
o_cfg = PC.PlotOutput(name, desc, otype, output_opts)
o_cfg.outdir = outdir
try:
layers = o_obj['layers']
except KeyError:
layers = []
for l in layers:
o_cfg.layers.append(self._parse_layer(l))
return o_cfg
def read(self, fstream):
"""
Read a file object into a config object
:param fstream: file stream of a config YAML file
"""
try:
data = yaml.load(fstream)
except yaml.YAMLError as e:
raise self.YamlError("Error loading YAML")
return None
self._check_version(data)
try:
outdir = data['options']['basedir']
except KeyError:
outdir = ""
# relative to CWD (absolute path overrides)
outdir = os.path.join(os.getcwd(), outdir)
cfg = PC.PlotConfig()
cfg.outdir = outdir
for o in data['outputs']:
op_cfg = self._parse_output(o)
cfg.add_output(op_cfg)
return cfg

View File

@ -2,8 +2,152 @@
Main Kiplot code
"""
import logging
import os
class KiPlot(object):
from . import plot_config as PCfg
try:
import pcbnew
except ImportError:
logging.error("Failed to import pcbnew Python module."
" Do you need to add it to PYTHONPATH?")
raise
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):
logging.debug("Starting plot of board {}".format(brd_file))
board = pcbnew.LoadBoard(brd_file)
logging.debug("Board loaded")
for op in self.cfg.outputs:
logging.debug("Processing output: {}".format(op.name))
# fresh plot controller
pc = pcbnew.PLOT_CONTROLLER(board)
if self._output_is_layer(op):
self._do_layer_plot(board, pc, op)
elif self._output_is_drill(op):
self._do_drill_plot(board, pc, op)
else:
raise ValueError("Don't know how to plot type {}"
.format(op.options.type))
pc.ClosePlot()
def _output_is_layer(self, output):
return output.options.type in [PCfg.OutputOptions.GERBER]
def _output_is_drill(self, output):
return output.options.type in [PCfg.OutputOptions.EXCELLON]
def _get_plot_format(self, output):
"""
Gets the Pcbnew plot format for a given KiPlot output type
"""
if output.options.type == PCfg.OutputOptions.GERBER:
return pcbnew.PLOT_FORMAT_GERBER
raise ValueError("Don't know how to translate plot type: {}"
.format(output.options.type))
def _do_layer_plot(self, board, plot_ctrl, output):
self._configure_plot_ctrl(plot_ctrl, output)
layer_cnt = board.GetCopperLayerCount()
# 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 ValueError(
"Inner layer {} is not valid for this board"
.format(layer.layer))
# Set current layer
plot_ctrl.SetLayer(layer.layer)
# Plot single layer to file
plot_format = self._get_plot_format(output)
plot_ctrl.OpenPlotfile(suffix, plot_format, desc)
logging.debug("Plotting layer {} to {}".format(
layer.layer, plot_ctrl.GetPlotFileName()))
plot_ctrl.PlotLayer()
def _do_drill_plot(self, board, plot_ctrl, output):
def __init__(self):
pass
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)
def _configure_plot_ctrl(self, plot_ctrl, output):
logging.debug("Configuring plot controller for output")
po = plot_ctrl.GetPlotOptions()
opts = output.options.type_options
# Set some important plot options:
po.SetPlotFrameRef(False)
# Line width for items without one defined
po.SetLineWidth(opts.line_width)
po.SetAutoScale(False) # do not change it
po.SetScale(1) # do not change it
po.SetMirror(False)
po.SetExcludeEdgeLayer(opts.exclude_edge_layer)
po.SetPlotPadsOnSilkLayer(not opts.exclude_pads_from_silkscreen)
po.SetUseAuxOrigin(opts.use_aux_axis_as_origin)
po.SetUseGerberAttributes(False)
if output.options.type == PCfg.OutputOptions.GERBER:
self._configure_gerber_opts(po, output)
# Disable plot pad holes
po.SetDrillMarksType(pcbnew.PCB_PLOT_PARAMS.NO_DRILL_SHAPE)
# Skip plot pad NPTH when possible: when drill size and shape == pad size
# and shape
# usually sel to True for copper layers
po.SetSkipPlotNPTH_Pads(False)
# outdir is a combination of the config and output
outdir = os.path.join(self.cfg.outdir, output.outdir)
logging.debug("Output destination: {}".format(outdir))
po.SetOutputDirectory(outdir)

108
kiplot/plot_config.py Normal file
View File

@ -0,0 +1,108 @@
import pcbnew
class LayerOptions(object):
"""
Common options that all layer outputs have
"""
def __init__(self):
self._line_width = None
self.exclude_edge_layer = False
self.exclude_pads_from_silkscreen = False
@property
def line_width(self):
return self._line_width
@line_width.setter
def line_width(self, value):
"""
Set the line width, in mm
"""
self._line_width = pcbnew.FromMM(value)
class GerberOptions(LayerOptions):
def __init__(self):
super(GerberOptions, self).__init__()
self.subtract_mask_from_silk = False
self.use_protel_extensions = False
class DrillOptions(object):
def __init__(self):
pass
class ExcellonOptions(DrillOptions):
def __init__(self):
super(ExcellonOptions, self).__init__()
self.metric_units = True
class OutputOptions(object):
GERBER = 'gerber'
EXCELLON = 'excellon'
def __init__(self, otype):
self.type = otype
if otype == self.GERBER:
self.type_options = GerberOptions()
elif otype == self.EXCELLON:
self.type_options = ExcellonOptions()
else:
self.type_options = None
class LayerInfo(object):
def __init__(self, layer, is_inner):
self.layer = layer
self.is_inner = is_inner
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 = []
class PlotConfig(object):
def __init__(self):
self._outputs = []
self.outdir = None
def add_output(self, new_op):
self._outputs.append(new_op)
@property
def outputs(self):
return self._outputs