Getting there
This commit is contained in:
parent
21e185c9d6
commit
5298dc2c72
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
148
kiplot/kiplot.py
148
kiplot/kiplot.py
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue