Merged in sopak/flatcam/tcl-commands (pull request #38)

Tcl commands error handling  fix
This commit is contained in:
jpcgt 2016-04-10 14:33:05 -04:00
commit 841a45e145
6 changed files with 195 additions and 41 deletions

View File

@ -1,4 +1,4 @@
import sys
import sys, traceback
import urllib
import getopt
import random
@ -286,6 +286,8 @@ class App(QtCore.QObject):
"cncjob_tooldia": 0.016,
"cncjob_prepend": "",
"cncjob_append": "",
"background_timeout": 300000, #default value is 5 minutes
"verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future).
# Persistence
"last_folder": None,
@ -679,12 +681,12 @@ class App(QtCore.QObject):
"""
this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command
"""
pass
def raise_tcl_unknown_error(self, unknownException):
"""
raise Exception if is different type than TclErrorException
this is here mainly to show unknown errors inside TCL shell console
:param unknownException:
:return:
"""
@ -694,6 +696,40 @@ class App(QtCore.QObject):
else:
raise unknownException
def display_tcl_error(self, error, error_info=None):
"""
escape bracket [ with \ otherwise there is error
"ERROR: missing close-bracket" instead of real error
:param error: it may be text or exception
:return: None
"""
if isinstance(error, Exception):
exc_type, exc_value, exc_traceback = error_info
if not isinstance(error, self.TclErrorException):
show_trace = 1
else:
show_trace = int(self.defaults['verbose_error_level'])
if show_trace > 0:
trc=traceback.format_list(traceback.extract_tb(exc_traceback))
trc_formated=[]
for a in reversed(trc):
trc_formated.append(a.replace(" ", " > ").replace("\n",""))
text="%s\nPython traceback: %s\n%s" % (exc_value,
exc_type,
"\n".join(trc_formated))
else:
text="%s" % error
else:
text=error
text = text.replace('[', '\\[').replace('"','\\"')
self.tcl.eval('return -code error "%s"' % text)
def raise_tcl_error(self, text):
"""
this method pass exception from python into TCL as error, so we get stacktrace and reason
@ -701,7 +737,7 @@ class App(QtCore.QObject):
:return: raise exception
"""
self.tcl.eval('return -code error "%s"' % text)
self.display_tcl_error(text)
raise self.TclErrorException(text)
def exec_command(self, text):

View File

@ -2777,7 +2777,7 @@ class CNCjob(Geometry):
# so we actually are sorting the tools by diameter
sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
if tools == "all":
tools = str([i[0] for i in sorted_tools]) # we get a string of ordered tools
tools = [i[0] for i in sorted_tools] # we get a array of ordered tools
log.debug("Tools 'all' and sorted are: %s" % str(tools))
else:
selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ','
@ -2818,24 +2818,26 @@ class CNCjob(Geometry):
for tool in tools:
# Tool change sequence (optional)
if toolchange:
gcode += "G00 Z%.4f\n" % toolchangez
gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer)
gcode += "M5\n" # Spindle Stop
gcode += "M6\n" # Tool change
gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"]
gcode += "M0\n" # Temporary machine stop
if self.spindlespeed is not None:
gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
else:
gcode += "M03\n" # Spindle start
# only if tool have some points, otherwise thre may be error and this part is useless
if tool in points:
# Tool change sequence (optional)
if toolchange:
gcode += "G00 Z%.4f\n" % toolchangez
gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer)
gcode += "M5\n" # Spindle Stop
gcode += "M6\n" # Tool change
gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"]
gcode += "M0\n" # Temporary machine stop
if self.spindlespeed is not None:
gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed
else:
gcode += "M03\n" # Spindle start
# Drillling!
for point in points[tool]:
x, y = point.coords.xy
gcode += t % (x[0], y[0])
gcode += down + up
# Drillling!
for point in points[tool]:
x, y = point.coords.xy
gcode += t % (x[0], y[0])
gcode += down + up
gcode += t % (0, 0)
gcode += "M05\n" # Spindle stop

View File

@ -125,6 +125,10 @@ class TclCommand(object):
for key, value in self.help['args'].items():
help_string.append(get_decorated_argument(key, value))
# timeout is unique for signaled commands (this is not best oop practice, but much easier for now)
if isinstance(self, TclCommandSignaled):
help_string.append("\t[-timeout <int>: Max wait for job timeout before error.]")
for example in self.help['examples']:
help_string.append(get_decorated_example(example))
@ -192,10 +196,13 @@ class TclCommand(object):
# check options
for key in options:
if key not in self.option_types and key is not 'timeout':
if key not in self.option_types and key != 'timeout':
self.raise_tcl_error('Unknown parameter: %s' % key)
try:
named_args[key] = self.option_types[key](options[key])
if key != 'timeout':
named_args[key] = self.option_types[key](options[key])
else:
named_args[key] = int(options[key])
except Exception, e:
self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
% (key, self.option_types[key], str(e)))
@ -207,6 +214,31 @@ class TclCommand(object):
return named_args, unnamed_args
def raise_tcl_unknown_error(self, unknownException):
"""
raise Exception if is different type than TclErrorException
this is here mainly to show unknown errors inside TCL shell console
:param unknownException:
:return:
"""
#if not isinstance(unknownException, self.TclErrorException):
# self.raise_tcl_error("Unknown error: %s" % str(unknownException))
#else:
raise unknownException
def raise_tcl_error(self, text):
"""
this method pass exception from python into TCL as error, so we get stacktrace and reason
:param text: text of error
:return: raise exception
"""
# becouse of signaling we cannot call error to TCL from here but when task is finished
# also nonsiglaned arwe handled here to better exception handling and diplay after command is finished
raise self.app.TclErrorException(text)
def execute_wrapper(self, *args):
"""
Command which is called by tcl console when current commands aliases are hit.
@ -225,8 +257,10 @@ class TclCommand(object):
args, unnamed_args = self.check_args(args)
return self.execute(args, unnamed_args)
except Exception as unknown:
error_info=sys.exc_info()
self.log.error("TCL command '%s' failed." % str(self))
self.app.raise_tcl_unknown_error(unknown)
self.app.display_tcl_error(unknown, error_info)
self.raise_tcl_unknown_error(unknown)
@abc.abstractmethod
def execute(self, args, unnamed_args):
@ -242,7 +276,6 @@ class TclCommand(object):
raise NotImplementedError("Please Implement this method")
class TclCommandSignaled(TclCommand):
"""
!!! I left it here only for demonstration !!!
@ -258,15 +291,18 @@ class TclCommandSignaled(TclCommand):
it handles all neccessary stuff about blocking and passing exeptions
"""
# default timeout for operation is 10 sec, but it can be much more
default_timeout = 10000
output = None
def execute_call(self, args, unnamed_args):
try:
self.output = None
self.error=None
self.error_info=None
self.output = self.execute(args, unnamed_args)
except Exception as unknown:
self.error_info = sys.exc_info()
self.error=unknown
finally:
self.app.shell_command_finished.emit(self)
@ -281,7 +317,7 @@ class TclCommandSignaled(TclCommand):
"""
@contextmanager
def wait_signal(signal, timeout=10000):
def wait_signal(signal, timeout=300000):
"""Block loop until signal emitted, or timeout (ms) elapses."""
loop = QtCore.QEventLoop()
@ -318,10 +354,10 @@ class TclCommandSignaled(TclCommand):
# Restore exception management
sys.excepthook = oeh
if ex:
self.raise_tcl_error(str(ex[0]))
raise ex[0]
if status['timed_out']:
self.app.raise_tcl_unknown_error('Operation timed out!')
self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option '-timeout <miliseconds>' for command or 'set_sys background_timeout <miliseconds>'.")
try:
self.log.debug("TCL command '%s' executed." % str(self.__class__))
@ -331,17 +367,15 @@ class TclCommandSignaled(TclCommand):
passed_timeout=args['timeout']
del args['timeout']
else:
passed_timeout=self.default_timeout
passed_timeout= self.app.defaults['background_timeout']
# set detail for processing, it will be there until next open or close
self.app.shell.open_proccessing(self.get_current_command())
self.output = None
def handle_finished(obj):
self.app.shell_command_finished.disconnect(handle_finished)
# TODO: handle output
pass
if self.error is not None:
self.raise_tcl_unknown_error(self.error)
self.app.shell_command_finished.connect(handle_finished)
@ -354,5 +388,7 @@ class TclCommandSignaled(TclCommand):
return self.output
except Exception as unknown:
error_info=sys.exc_info()
self.log.error("TCL command '%s' failed." % str(self))
self.app.raise_tcl_unknown_error(unknown)
self.app.display_tcl_error(unknown, error_info)
self.raise_tcl_unknown_error(unknown)

View File

@ -49,8 +49,7 @@ class TclCommandCncjob(TclCommand.TclCommandSignaled):
('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
('multidepth', 'Use or not multidepth cnccut.'),
('depthperpass', 'Height of one layer for multidepth.'),
('outname', 'Name of the resulting Geometry object.'),
('timeout', 'Max wait for job timeout before error.')
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}

View File

@ -0,0 +1,81 @@
from ObjectCollection import *
import TclCommand
class TclCommandDrillcncjob(TclCommand.TclCommandSignaled):
"""
Tcl shell command to Generates a Drill CNC Job from a Excellon Object.
"""
# array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
aliases = ['drillcncjob']
# dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([
('name', str)
])
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('tools',str),
('drillz',float),
('travelz',float),
('feedrate',float),
('spindlespeed',int),
('toolchange',bool),
('outname',str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name']
# structured help for current command, args needs to be ordered
help = {
'main': "Generates a Drill CNC Job from a Excellon Object.",
'args': collections.OrderedDict([
('name', 'Name of the source object.'),
('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'),
('drillz', 'Drill depth into material (example: -2.0).'),
('travelz', 'Travel distance above material (example: 2.0).'),
('feedrate', 'Drilling feed rate.'),
('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
('toolchange', 'Enable tool changes (example: True).'),
('outname', 'Name of the resulting Geometry object.')
]),
'examples': []
}
def execute(self, args, unnamed_args):
"""
execute current TCL shell command
:param args: array of known named arguments and options
:param unnamed_args: array of other values which were passed into command
without -somename and we do not have them in known arg_names
:return: None or exception
"""
name = args['name']
if 'outname' not in args:
args['outname'] = name + "_cnc"
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)
if not isinstance(obj, FlatCAMExcellon):
self.raise_tcl_error('Expected FlatCAMExcellon, got %s %s.' % (name, type(obj)))
def job_init(job_obj, app):
job_obj.z_cut = args["drillz"]
job_obj.z_move = args["travelz"]
job_obj.feedrate = args["feedrate"]
job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None
toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
tools = args["tools"] if "tools" in args else 'all'
job_obj.generate_from_excellon_by_tool(obj, tools, toolchange)
job_obj.gcode_parse()
job_obj.create_geometry()
self.app.new_object("cncjob", args['outname'], job_init)

View File

@ -1,10 +1,11 @@
import pkgutil
import sys
# allowed command modules
# allowed command modules (please append them alphabetically ordered)
import tclCommands.TclCommandAddPolygon
import tclCommands.TclCommandAddPolyline
import tclCommands.TclCommandCncjob
import tclCommands.TclCommandDrillcncjob
import tclCommands.TclCommandExportGcode
import tclCommands.TclCommandExteriors
import tclCommands.TclCommandInteriors
@ -19,7 +20,6 @@ for loader, name, is_pkg in pkgutil.walk_packages(__path__):
module = loader.find_module(name).load_module(name)
__all__.append(name)
def register_all_commands(app, commands):
"""
Static method which register all known commands.