Better debug info, fixed errors and no .kicad_pcb dependency

- When a BoM operation fails now we show the output of the child process.
  (Only enabled when using debug verbosity)
- The error levels 1 and 2 were overlapped with internal Python codes.
- Now we delay the PCB load until we really need it. Which could be never.
This commit is contained in:
Salvador E. Tropea 2020-06-12 15:10:56 -03:00
parent aef19e31c7
commit 61f1ebbab2
7 changed files with 255 additions and 74 deletions

View File

@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Better debug information when a BoM fails to be generated.
### Changed
- Allowed operations that doesn't involve a PCB now can run if the PCB file is
missing or corrupted.
### Fixed
- Error codes that overlapped.
## [0.2.5] - 2020-06-11
### Added

View File

@ -42,6 +42,11 @@ def plot_error(msg):
exit(misc.PLOT_ERROR)
def level_debug():
""" Determine if we are in debug mode """
return logger.getEffectiveLevel() <= logging.DEBUG
def check_version(command, version):
cmd = [command, '--version']
logger.debug('Running: '+str(cmd))
@ -87,11 +92,6 @@ class Plotter(object):
def plot(self, brd_file, target, invert, skip_pre):
logger.debug("Starting plot of board {}".format(brd_file))
board = pcbnew.LoadBoard(brd_file)
assert board is not None
logger.debug("Board loaded")
self._preflight_checks(brd_file, skip_pre)
n = len(target)
@ -100,29 +100,29 @@ class Plotter(object):
logger.debug('Skipping all outputs')
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))
# fresh plot controller
pc = pcbnew.PLOT_CONTROLLER(board)
self._configure_output_dir(pc, op)
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, pc, op, brd_file)
self._do_layer_plot(board, output_dir, op, brd_file)
elif self._output_is_drill(op):
self._do_drill_plot(board, pc, op)
self._do_drill_plot(board, output_dir, op)
elif self._output_is_position(op):
self._do_position_plot(board, pc, op)
self._do_position_plot(board, output_dir, op)
elif self._output_is_bom(op):
self._do_bom(board, pc, op, brd_file)
self._do_bom(output_dir, op, brd_file)
elif self._output_is_sch_print(op):
self._do_sch_print(board, pc, op, brd_file)
self._do_sch_print(output_dir, op, brd_file)
elif self._output_is_pcb_print(op):
self._do_pcb_print(board, pc, op, brd_file)
self._do_pcb_print(board, 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)
@ -131,6 +131,17 @@ class Plotter(object):
else:
logger.debug('Skipping %s output', op.name)
def _load_board(self, brd_file):
try:
board = pcbnew.LoadBoard(brd_file)
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")
@ -178,7 +189,7 @@ class Plotter(object):
cmd.extend(['-f', filter_file])
cmd.extend([sch_file, self.cfg.outdir])
# If we are in verbose mode enable debug in the child
if logger.getEffectiveLevel() <= logging.DEBUG:
if level_debug():
cmd.insert(1, '-vv')
cmd.insert(1, '-r')
logger.info('- Running the ERC')
@ -195,7 +206,7 @@ class Plotter(object):
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 logger.getEffectiveLevel() <= logging.DEBUG:
if level_debug():
cmd.insert(1, '-vv')
cmd.insert(1, '-r')
logger.info('- Updating BoM in XML format')
@ -212,7 +223,7 @@ class Plotter(object):
cmd.extend(['-f', filter_file])
cmd.extend([brd_file, self.cfg.outdir])
# If we are in verbose mode enable debug in the child
if logger.getEffectiveLevel() <= logging.DEBUG:
if level_debug():
cmd.insert(1, '-vv')
cmd.insert(1, '-r')
if ignore_unconnected:
@ -230,7 +241,7 @@ class Plotter(object):
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,
@ -241,7 +252,7 @@ class Plotter(object):
]
def _output_is_drill(self, output):
""" All the drill formats' """
return output.options.type in [
PCfg.OutputOptions.EXCELLON,
PCfg.OutputOptions.GERB_DRILL,
@ -262,6 +273,10 @@ class Plotter(object):
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
@ -284,10 +299,12 @@ class Plotter(object):
raise ValueError("Don't know how to translate plot type: {}"
.format(output.options.type))
def _do_layer_plot(self, board, plot_ctrl, output, file_name):
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)
self._configure_plot_ctrl(plot_ctrl, output, output_dir)
po = plot_ctrl.GetPlotOptions()
layer_cnt = board.GetCopperLayerCount()
@ -373,12 +390,10 @@ class Plotter(object):
return drill_writer
def _do_drill_plot(self, board, plot_ctrl, output):
def _do_drill_plot(self, board, output_dir, output):
to = output.options.type_options
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
# dialog_gendrill.cpp:357
if to.use_aux_axis_as_origin:
offset = board.GetAuxOrigin()
@ -399,37 +414,36 @@ class Plotter(object):
gen_report = to.generate_report
if gen_drill:
logger.debug("Generating drill files in "+outdir)
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, outdir))
.format(to.map_options.type, output_dir))
drill_writer.CreateDrillandMapFilesSet(outdir, gen_drill, gen_map)
drill_writer.CreateDrillandMapFilesSet(output_dir, gen_drill, gen_map)
if gen_report:
drill_report_file = os.path.join(outdir,
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, plot_ctrl, output, columns,
def _do_position_plot_ascii(self, board, output_dir, output, columns,
modulesStr, maxSizes):
to = output.options.type_options
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
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(outdir, "{}-top.pos".format(name)), 'w')
botf = open(os.path.join(outdir, "{}-bottom.pos".format(name)),
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(outdir, "{}-both.pos").format(name), 'w')
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:
@ -482,22 +496,21 @@ class Plotter(object):
if bothf is not None:
bothf.close()
def _do_position_plot_csv(self, board, plot_ctrl, output, columns,
def _do_position_plot_csv(self, board, output_dir, output, columns,
modulesStr):
to = output.options.type_options
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
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(outdir, "{}-top-pos.csv".format(name)),
topf = open(os.path.join(output_dir, "{}-top-pos.csv".format(name)),
'w')
botf = open(os.path.join(outdir, "{}-bottom-pos.csv".format(name)),
botf = open(os.path.join(output_dir, "{}-bottom-pos.csv".format(name)),
'w')
else:
bothf = open(os.path.join(outdir, "{}-both-pos.csv").format(name),
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]
@ -523,7 +536,7 @@ class Plotter(object):
if bothf is not None:
bothf.close()
def _do_position_plot(self, board, plot_ctrl, output):
def _do_position_plot(self, board, output_dir, output):
to = output.options.type_options
columns = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"]
@ -562,18 +575,17 @@ class Plotter(object):
# Note: the parser already checked the format is ASCII or CSV
if to.format.lower() == 'ascii':
self._do_position_plot_ascii(board, plot_ctrl, output, columns,
self._do_position_plot_ascii(board, output_dir, output, columns,
modules, maxlengths)
else: # if to.format.lower() == 'csv':
self._do_position_plot_csv(board, plot_ctrl, output, columns,
self._do_position_plot_csv(board, output_dir, output, columns,
modules)
def _do_sch_print(self, board, plot_ctrl, output, brd_file):
def _do_sch_print(self, output_dir, output, brd_file):
sch_file = check_eeschema_do(brd_file)
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
cmd = [misc.CMD_EESCHEMA_DO, 'export', '--all_pages',
'--file_format', 'pdf', sch_file, outdir]
if logger.getEffectiveLevel() <= logging.DEBUG:
'--file_format', 'pdf', sch_file, output_dir]
if level_debug():
cmd.insert(1, '-vv')
cmd.insert(1, '-r')
logger.debug('Executing: '+str(cmd))
@ -583,12 +595,12 @@ class Plotter(object):
exit(misc.PDF_SCH_PRINT)
to = output.options.type_options
if to.output:
cur = os.path.abspath(os.path.join(outdir, os.path.splitext(os.path.basename(brd_file))[0]) + '.pdf')
new = os.path.abspath(os.path.join(outdir, 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_pcb_print(self, board, plot_ctrl, output, brd_file):
def _do_pcb_print(self, board, output_dir, output, brd_file):
check_script(misc.CMD_PCBNEW_PRINT_LAYERS,
misc.URL_PCBNEW_PRINT_LAYERS, '1.3.1')
to = output.options.type_options
@ -602,11 +614,10 @@ class Plotter(object):
raise PlotError(
"Inner layer {} is not valid for this board"
.format(layer.layer))
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
cmd = [misc.CMD_PCBNEW_PRINT_LAYERS, 'export',
'--output_name', to.output_name,
brd_file, outdir]
if logger.getEffectiveLevel() <= logging.DEBUG:
brd_file, output_dir]
if level_debug():
cmd.insert(1, '-vv')
cmd.insert(1, '-r')
# Add the layers
@ -618,37 +629,38 @@ class Plotter(object):
logger.error(misc.CMD_PCBNEW_PRINT_LAYERS+' returned %d', ret)
exit(misc.PDF_PCB_PRINT)
def _do_bom(self, board, plot_ctrl, output, brd_file):
def _do_bom(self, output_dir, output, brd_file):
if output.options.type == 'kibom':
self._do_kibom(board, plot_ctrl, output, brd_file)
self._do_kibom(output_dir, output, brd_file)
else:
self._do_ibom(board, plot_ctrl, output, brd_file)
self._do_ibom(output_dir, output, brd_file)
def _do_kibom(self, board, plot_ctrl, 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()
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
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(outdir, os.path.basename(prj))+'.'+format]
os.path.join(output_dir, os.path.basename(prj))+'.'+format]
logger.debug('Running: '+str(cmd))
try:
check_output(cmd, stderr=STDOUT)
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(outdir, prj)+'*.tmp'):
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, board, plot_ctrl, output, brd_file):
def _do_ibom(self, output_dir, output, brd_file):
check_script(misc.CMD_IBOM, misc.URL_IBOM)
outdir = plot_ctrl.GetPlotOptions().GetOutputDirectory()
prj = os.path.splitext(os.path.relpath(brd_file))[0]
logger.debug('Doing Interactive BoM, prj: '+prj)
cmd = [misc.CMD_IBOM, brd_file,
'--dest-dir', outdir,
'--dest-dir', output_dir,
'--no-browser', ]
to = output.options.type_options
if to.blacklist:
@ -659,10 +671,13 @@ class Plotter(object):
cmd.append(to.name_format)
logger.debug('Running: '+str(cmd))
try:
check_output(cmd, stderr=STDOUT)
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):
@ -704,7 +719,7 @@ class Plotter(object):
po.SetDXFPlotPolygonMode(dxf_opts.polygon_mode)
def _configure_output_dir(self, plot_ctrl, output):
def _get_output_dir(self, output):
# outdir is a combination of the config and output
outdir = os.path.join(self.cfg.outdir, output.outdir)
@ -713,14 +728,14 @@ class Plotter(object):
if not os.path.exists(outdir):
os.makedirs(outdir)
po = plot_ctrl.GetPlotOptions()
po.SetOutputDirectory(outdir)
return outdir
def _configure_plot_ctrl(self, plot_ctrl, output):
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

View File

@ -2,8 +2,8 @@
"""
# Error levels
NO_YAML_MODULE = 1
NO_PCBNEW_MODULE = 2
INTERNAL_ERROR = 1 # Unhandled exceptions
WRONG_ARGUMENTS = 2 # This is what argsparse uses
USUPPORTED_OPTION = 3
MISSING_TOOL = 4
DRC_ERROR = 5
@ -16,6 +16,9 @@ BOM_ERROR = 11
PDF_SCH_PRINT = 12
PDF_PCB_PRINT = 13
PLOT_ERROR = 14
NO_YAML_MODULE = 15
NO_PCBNEW_MODULE = 16
CORRUPTED_PCB = 17
CMD_EESCHEMA_DO = 'eeschema_do'
URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/kicad-automation-scripts'

View File

@ -0,0 +1 @@
bogus

View File

@ -0,0 +1,139 @@
EESchema Schematic File Version 4
EELAYER 30 0
EELAYER END
$Descr A4 11693 8268
encoding utf-8
Sheet 1 1
Title ""
Date ""
Rev ""
Comp ""
Comment1 ""
Comment2 ""
Comment3 ""
Comment4 ""
$EndDescr
$Comp
L Device:R R1
U 1 1 5EBE8A2E
P 3500 2200
F 0 "R1" H 3570 2246 50 0000 L CNN
F 1 "100" H 3570 2155 50 0000 L CNN
F 2 "Resistor_SMD:R_0805_2012Metric" V 3430 2200 50 0001 C CNN
F 3 "~" H 3500 2200 50 0001 C CNN
1 3500 2200
1 0 0 -1
$EndComp
$Comp
L Device:R R2
U 1 1 5EBE8E9E
P 3500 2750
F 0 "R2" H 3570 2796 50 0000 L CNN
F 1 "200" H 3570 2705 50 0000 L CNN
F 2 "Resistor_SMD:R_0805_2012Metric" V 3430 2750 50 0001 C CNN
F 3 "~" H 3500 2750 50 0001 C CNN
1 3500 2750
1 0 0 -1
$EndComp
$Comp
L Device:C C1
U 1 1 5EBE91AC
P 3900 2750
F 0 "C1" H 4015 2796 50 0000 L CNN
F 1 "1uF" H 4015 2705 50 0000 L CNN
F 2 "Capacitor_SMD:C_0805_2012Metric" H 3938 2600 50 0001 C CNN
F 3 "~" H 3900 2750 50 0001 C CNN
1 3900 2750
1 0 0 -1
$EndComp
Wire Wire Line
3500 2350 3500 2450
Wire Wire Line
3900 2600 3900 2450
Wire Wire Line
3900 2450 3500 2450
Connection ~ 3500 2450
Wire Wire Line
3500 2450 3500 2600
$Comp
L power:GND #PWR03
U 1 1 5EBE965A
P 3900 3000
F 0 "#PWR03" H 3900 2750 50 0001 C CNN
F 1 "GND" H 3905 2827 50 0000 C CNN
F 2 "" H 3900 3000 50 0001 C CNN
F 3 "" H 3900 3000 50 0001 C CNN
1 3900 3000
1 0 0 -1
$EndComp
$Comp
L power:GND #PWR02
U 1 1 5EBE9830
P 3500 3000
F 0 "#PWR02" H 3500 2750 50 0001 C CNN
F 1 "GND" H 3505 2827 50 0000 C CNN
F 2 "" H 3500 3000 50 0001 C CNN
F 3 "" H 3500 3000 50 0001 C CNN
1 3500 3000
1 0 0 -1
$EndComp
$Comp
L power:VCC #PWR01
U 1 1 5EBE99A0
P 3500 1950
F 0 "#PWR01" H 3500 1800 50 0001 C CNN
F 1 "VCC" H 3517 2123 50 0000 C CNN
F 2 "" H 3500 1950 50 0001 C CNN
F 3 "" H 3500 1950 50 0001 C CNN
1 3500 1950
1 0 0 -1
$EndComp
Wire Wire Line
3500 2050 3500 2000
Wire Wire Line
3500 3000 3500 2900
Wire Wire Line
3900 3000 3900 2900
$Comp
L power:GND #PWR0101
U 1 1 5EC534BF
P 4500 3050
F 0 "#PWR0101" H 4500 2800 50 0001 C CNN
F 1 "GND" H 4505 2877 50 0000 C CNN
F 2 "" H 4500 3050 50 0001 C CNN
F 3 "" H 4500 3050 50 0001 C CNN
1 4500 3050
1 0 0 -1
$EndComp
$Comp
L power:PWR_FLAG #FLG0101
U 1 1 5EC53A6E
P 4500 2950
F 0 "#FLG0101" H 4500 3025 50 0001 C CNN
F 1 "PWR_FLAG" H 4500 3123 50 0000 C CNN
F 2 "" H 4500 2950 50 0001 C CNN
F 3 "~" H 4500 2950 50 0001 C CNN
1 4500 2950
1 0 0 -1
$EndComp
$Comp
L power:PWR_FLAG #FLG0102
U 1 1 5EC53E1A
P 3200 1950
F 0 "#FLG0102" H 3200 2025 50 0001 C CNN
F 1 "PWR_FLAG" H 3200 2123 50 0000 C CNN
F 2 "" H 3200 1950 50 0001 C CNN
F 3 "~" H 3200 1950 50 0001 C CNN
1 3200 1950
1 0 0 -1
$EndComp
Wire Wire Line
3200 1950 3200 2000
Wire Wire Line
3200 2000 3500 2000
Connection ~ 3500 2000
Wire Wire Line
3500 2000 3500 1950
Wire Wire Line
4500 2950 4500 3050
$EndSCHEMATC

View File

@ -13,10 +13,12 @@ pytest-3 --log-cli-level debug
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(prev_dir))
# Utils import
from utils import context
sys.path.insert(0, os.path.dirname(prev_dir))
from kiplot.misc import (BOM_ERROR)
BOM_DIR = 'BoM'
@ -35,3 +37,9 @@ def test_bom():
ctx.search_in_file(csv, ['R,R1,100', 'R,R2,200', 'C,C1,1uF'])
os.remove(os.path.join(ctx.get_board_dir(), 'bom.ini'))
ctx.clean_up()
def test_bom_fail():
ctx = context.TestContext('BoM_fail', 'bom_no_xml', 'bom', BOM_DIR)
ctx.run(BOM_ERROR)
ctx.clean_up()

View File

@ -13,10 +13,12 @@ pytest-3 --log-cli-level debug
import os
import sys
# Look for the 'utils' module from where the script is running
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(script_dir))
prev_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(prev_dir))
# Utils import
from utils import context
sys.path.insert(0, os.path.dirname(prev_dir))
from kiplot.misc import (BOM_ERROR)
BOM_DIR = 'BoM'
@ -39,3 +41,9 @@ def test_ibom_no_ops():
ctx.run()
ctx.expect_out_file(os.path.join(BOM_DIR, 'ibom.html'))
ctx.clean_up()
def test_ibom_fail():
ctx = context.TestContext('BoM_interactiveFail', 'bom_no_xml', 'ibom', BOM_DIR)
ctx.run(BOM_ERROR)
ctx.clean_up()