From a4845d150e5e031e373b8670bb6572c977f12b48 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 10:43:06 +0200 Subject: [PATCH 1/8] add important comment --- FlatCAMApp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d31a7044..32b501e5 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -685,6 +685,7 @@ class App(QtCore.QObject): 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: """ From b98954dccd383f178b1a6b58e4e491281e4045c1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 14:20:50 +0200 Subject: [PATCH 2/8] fix error handling in signaled commands, error gets info about different scoup instead of true error more detaild error print including python trace when more complex unknown error reinplement drillcncjob fix camlib problem with all drills("all" was already there) but it crashes on tools without points, when no tools "all" is as default add timeout to all helps if command is signaled --- FlatCAMApp.py | 28 +++++++++- camlib.py | 36 +++++++------ tclCommands/TclCommand.py | 57 ++++++++++++++++---- tclCommands/TclCommandCncjob.py | 3 +- tclCommands/TclCommandDrillcncjob.py | 81 ++++++++++++++++++++++++++++ tclCommands/__init__.py | 4 +- 6 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 tclCommands/TclCommandDrillcncjob.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 32b501e5..020ec593 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1,4 +1,4 @@ -import sys +import sys, traceback import urllib import getopt import random @@ -695,6 +695,30 @@ 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 + 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=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 @@ -702,7 +726,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): diff --git a/camlib.py b/camlib.py index 2a717ea6..0d1bd383 100644 --- a/camlib.py +++ b/camlib.py @@ -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 diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index f713b319..57e4a9d1 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -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 : 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 !!! @@ -266,7 +299,13 @@ class TclCommandSignaled(TclCommand): 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) @@ -336,12 +375,12 @@ class TclCommandSignaled(TclCommand): # 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.log.error("TCL command '%s' failed." % str(self)) + self.app.display_tcl_error(self.error, self.error_info) + self.raise_tcl_unknown_error(self.error) self.app.shell_command_finished.connect(handle_finished) @@ -355,4 +394,4 @@ class TclCommandSignaled(TclCommand): except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) - self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file + self.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py index e088d0ec..e6d84de3 100644 --- a/tclCommands/TclCommandCncjob.py +++ b/tclCommands/TclCommandCncjob.py @@ -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': [] } diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py new file mode 100644 index 00000000..17441967 --- /dev/null +++ b/tclCommands/TclCommandDrillcncjob.py @@ -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: 1).'), + ('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", name, job_init) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index af67a9cd..2f733017 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -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. From c2cdaaf45234029ae43cc4a1b438258fad6decac Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 14:37:40 +0200 Subject: [PATCH 3/8] fix display also for nonsignaled exceptions in execute_wrapper --- tclCommands/TclCommand.py | 2 ++ tclCommands/TclCommandDrillcncjob.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 57e4a9d1..24f8295f 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -393,5 +393,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.display_tcl_error(unknown, error_info) self.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index 17441967..f65931fd 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -39,7 +39,7 @@ class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): ('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: 1).'), + ('toolchange', 'Enable tool changes (example: True).'), ('outname', 'Name of the resulting Geometry object.') ]), 'examples': [] From 5bd6432ead16b96dfe40a15134765d37b5364d45 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 6 Apr 2016 11:20:53 +0200 Subject: [PATCH 4/8] solve message in special tcl keywords used in wrong context as "unknown" --- FlatCAMApp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 020ec593..b2ee9745 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -762,6 +762,11 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") + + # solve message in special tcl keywords used in wrong context as "unknown" + if e.message == 'invalid command name ""': + result=result.replace('""','"%s"' % text.replace("\n","")) + self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return or in test raise exception From 4c20040fbe792cf247f2412e841d5e3aa1ecd99c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 9 Apr 2016 12:48:32 +0200 Subject: [PATCH 5/8] fix errors in tool selection --- camlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camlib.py b/camlib.py index 0d1bd383..7c0cd11a 100644 --- a/camlib.py +++ b/camlib.py @@ -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 ',' @@ -2819,7 +2819,7 @@ class CNCjob(Geometry): for tool in tools: # only if tool have some points, otherwise thre may be error and this part is useless - if "tool" in points: + if tool in points: # Tool change sequence (optional) if toolchange: gcode += "G00 Z%.4f\n" % toolchangez From fae9875dd896762c156dc18b1e38985f136ec639 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 11:09:26 +0200 Subject: [PATCH 6/8] remove unknown workaround --- FlatCAMApp.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index b2ee9745..020ec593 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -762,11 +762,6 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") - - # solve message in special tcl keywords used in wrong context as "unknown" - if e.message == 'invalid command name ""': - result=result.replace('""','"%s"' % text.replace("\n","")) - self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return or in test raise exception From 26a8b7347b2474ae5bb287091ddff2d2aa8db873 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 11:10:25 +0200 Subject: [PATCH 7/8] change default timeout fix outname bug in drillcncjob --- tclCommands/TclCommand.py | 4 ++-- tclCommands/TclCommandDrillcncjob.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 24f8295f..bc7cd2c2 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -291,8 +291,8 @@ 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 + # default timeout for operation is 300000 sec, but it can be much more + default_timeout = 300000 output = None diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index f65931fd..783b6599 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -78,4 +78,4 @@ class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): job_obj.gcode_parse() job_obj.create_geometry() - self.app.new_object("cncjob", name, job_init) + self.app.new_object("cncjob", args['outname'], job_init) From e236a60be90e9ca9bdc4be90ca6c4731dcdf8265 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 15:14:18 +0200 Subject: [PATCH 8/8] implement system values background_timeout and verbose_error_level implement correct error level handling based on verbose_error_level , fix double print of tcl error and do not wrap unknown exceptions into TCL known --- FlatCAMApp.py | 27 +++++++++++++++++++-------- tclCommands/TclCommand.py | 13 ++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 020ec593..872a6e4f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -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,7 +681,6 @@ 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): @@ -704,14 +705,24 @@ class App(QtCore.QObject): """ if isinstance(error, Exception): + exc_type, exc_value, exc_traceback = error_info - 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)) + 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 diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index bc7cd2c2..b93ec752 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -291,9 +291,6 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 300000 sec, but it can be much more - default_timeout = 300000 - output = None def execute_call(self, args, unnamed_args): @@ -320,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() @@ -357,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 ' for command or 'set_sys background_timeout '.") try: self.log.debug("TCL command '%s' executed." % str(self.__class__)) @@ -370,7 +367,7 @@ 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()) @@ -378,8 +375,6 @@ class TclCommandSignaled(TclCommand): def handle_finished(obj): self.app.shell_command_finished.disconnect(handle_finished) if self.error is not None: - self.log.error("TCL command '%s' failed." % str(self)) - self.app.display_tcl_error(self.error, self.error_info) self.raise_tcl_unknown_error(self.error) self.app.shell_command_finished.connect(handle_finished)