583 lines
18 KiB
Python
583 lines
18 KiB
Python
"""
|
|
Class to read KiPlot config files
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
import pcbnew
|
|
|
|
from . import plot_config as PC
|
|
from . import log
|
|
from . import misc
|
|
|
|
logger = log.get_logger(__name__)
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
log.init(False, False)
|
|
logger.error('No yaml module for Python, install python3-yaml')
|
|
sys.exit(misc.NO_YAML_MODULE)
|
|
|
|
|
|
class CfgReader(object):
|
|
def __init__(self):
|
|
pass
|
|
|
|
|
|
def config_error(msg):
|
|
logger.error(msg)
|
|
sys.exit(misc.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):
|
|
|
|
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.")
|
|
|
|
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):
|
|
|
|
# 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']
|
|
|
|
# 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'],
|
|
'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,
|
|
},
|
|
]
|
|
|
|
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,
|
|
'F.Adhes': pcbnew.F_Adhes,
|
|
'B.Adhes': pcbnew.B_Adhes,
|
|
'F.Paste': pcbnew.F_Paste,
|
|
'B.Paste': pcbnew.B_Paste,
|
|
'F.SilkS': pcbnew.F_SilkS,
|
|
'B.SilkS': pcbnew.B_SilkS,
|
|
'F.Mask': pcbnew.F_Mask,
|
|
'B.Mask': pcbnew.B_Mask,
|
|
'Dwgs.User': pcbnew.Dwgs_User,
|
|
'Cmts.User': pcbnew.Cmts_User,
|
|
'Eco1.User': pcbnew.Eco1_User,
|
|
'Eco2.User': pcbnew.Eco2_User,
|
|
'Edge.Cuts': pcbnew.Edge_Cuts,
|
|
'Margin': pcbnew.Margin,
|
|
'F.CrtYd': pcbnew.F_CrtYd,
|
|
'B.CrtYd': pcbnew.B_CrtYd,
|
|
'F.Fab': pcbnew.F_Fab,
|
|
'B.Fab': pcbnew.B_Fab,
|
|
}
|
|
|
|
layer = None
|
|
|
|
# Priority
|
|
# 1) Internal list
|
|
if s in D:
|
|
layer = PC.LayerInfo(D[s], False, s)
|
|
else:
|
|
try:
|
|
# 2) List from the PCB
|
|
id = self.layer_names.index(s)
|
|
layer = PC.LayerInfo(id, id < pcbnew.B_Cu, s)
|
|
except ValueError:
|
|
# 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')
|
|
|
|
layer = PC.LayerInfo(int(m.group(1)), True, s)
|
|
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
|
|
|
|
def _parse_output(self, o_obj):
|
|
|
|
try:
|
|
name = o_obj['name']
|
|
except KeyError:
|
|
config_error("Output needs a name in: "+str(o_obj))
|
|
|
|
try:
|
|
desc = o_obj['comment']
|
|
except KeyError:
|
|
desc = None
|
|
|
|
try:
|
|
otype = o_obj['type']
|
|
except KeyError:
|
|
config_error("Output '"+name+"' needs a type")
|
|
|
|
if otype not in ['gerber', 'ps', 'hpgl', 'dxf', 'pdf', 'svg',
|
|
'gerb_drill', 'excellon', 'position',
|
|
'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':
|
|
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_preflight(self, pf, cfg):
|
|
|
|
logger.debug("Parsing preflight options: {}".format(pf))
|
|
|
|
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']
|
|
|
|
def read(self, fstream):
|
|
"""
|
|
Read a file object into a config object
|
|
|
|
:param fstream: file stream of a config YAML file
|
|
"""
|
|
|
|
try:
|
|
data = yaml.load(fstream)
|
|
except yaml.YAMLError:
|
|
config_error("Error loading YAML")
|
|
|
|
self._check_version(data)
|
|
|
|
cfg = PC.PlotConfig()
|
|
|
|
if 'preflight' in data:
|
|
self._parse_preflight(data['preflight'], cfg)
|
|
|
|
for o in data['outputs']:
|
|
|
|
op_cfg = self._parse_output(o)
|
|
cfg.add_output(op_cfg)
|
|
|
|
return cfg
|