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.