Procházet zdrojové kódy

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

Tcl commands error handling  fix
jpcgt před 9 roky
rodič
revize
841a45e145

+ 39 - 3
FlatCAMApp.py

@@ -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):

+ 20 - 18
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 ','
@@ -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

+ 52 - 16
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 <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)

+ 1 - 2
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': []
     }

+ 81 - 0
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: 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)

+ 2 - 2
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.