Major code refactor

Pro:
- Much easier to add new outputs and pre-flights
- All options are optional
- Much better configuration syntax check
- Access to data is simpler

Cons:
- Much more source code files
- I focused on one application, not multiple instances running on the same
  process.
This commit is contained in:
Salvador E. Tropea 2020-06-19 15:54:55 -03:00
parent e34952a2a2
commit 7679604646
29 changed files with 1649 additions and 1907 deletions

View File

@ -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__":

View File

@ -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

View File

@ -5,3 +5,11 @@ KiPlot errors
class KiPlotError(Exception):
pass
class PlotError(KiPlotError):
pass
class KiPlotConfigurationError(KiPlotError):
pass

View File

@ -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))

88
kiplot/out_any_drill.py Normal file
View File

@ -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)

145
kiplot/out_any_layer.py Normal file
View File

@ -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

82
kiplot/out_base.py Normal file
View File

@ -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))

37
kiplot/out_dxf.py Normal file
View File

@ -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)

22
kiplot/out_excellon.py Normal file
View File

@ -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)

19
kiplot/out_gerb_drill.py Normal file
View File

@ -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)

43
kiplot/out_gerber.py Normal file
View File

@ -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)

38
kiplot/out_hpgl.py Normal file
View File

@ -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)

43
kiplot/out_ibom.py Normal file
View File

@ -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)

51
kiplot/out_kibom.py Normal file
View File

@ -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)

33
kiplot/out_pdf.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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)

171
kiplot/out_position.py Normal file
View File

@ -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)

46
kiplot/out_ps.py Normal file
View File

@ -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)

74
kiplot/out_step.py Normal file
View File

@ -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)

33
kiplot/out_svg.py Normal file
View File

@ -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)

View File

@ -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

82
kiplot/pre_base.py Normal file
View File

@ -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))

View File

@ -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)

47
kiplot/pre_drc.py Normal file
View File

@ -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)

45
kiplot/pre_erc.py Normal file
View File

@ -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)

22
kiplot/pre_filters.py Normal file
View File

@ -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)

View File

@ -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)

39
kiplot/pre_update_xml.py Normal file
View File

@ -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)