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:
parent
e34952a2a2
commit
7679604646
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,3 +5,11 @@ KiPlot errors
|
|||
|
||||
class KiPlotError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PlotError(KiPlotError):
|
||||
pass
|
||||
|
||||
|
||||
class KiPlotConfigurationError(KiPlotError):
|
||||
pass
|
||||
|
|
|
|||
901
kiplot/kiplot.py
901
kiplot/kiplot.py
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue