Просмотр исходного кода

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

Tcl commands error handling  fix
jpcgt 9 лет назад
Родитель
Сommit
841a45e145
6 измененных файлов с 195 добавлено и 41 удалено
  1. 39 3
      FlatCAMApp.py
  2. 20 18
      camlib.py
  3. 52 16
      tclCommands/TclCommand.py
  4. 1 2
      tclCommands/TclCommandCncjob.py
  5. 81 0
      tclCommands/TclCommandDrillcncjob.py
  6. 2 2
      tclCommands/__init__.py

+ 39 - 3
FlatCAMApp.py

@@ -1,4 +1,4 @@
-import sys
+import sys, traceback
 import urllib
 import urllib
 import getopt
 import getopt
 import random
 import random
@@ -286,6 +286,8 @@ class App(QtCore.QObject):
             "cncjob_tooldia": 0.016,
             "cncjob_tooldia": 0.016,
             "cncjob_prepend": "",
             "cncjob_prepend": "",
             "cncjob_append": "",
             "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
             # Persistence
             "last_folder": None,
             "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
         this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command
         """
         """
-
         pass
         pass
 
 
     def raise_tcl_unknown_error(self, unknownException):
     def raise_tcl_unknown_error(self, unknownException):
         """
         """
         raise Exception if is different type  than TclErrorException
         raise Exception if is different type  than TclErrorException
+        this is here mainly to show unknown errors inside TCL shell console
         :param unknownException:
         :param unknownException:
         :return:
         :return:
         """
         """
@@ -694,6 +696,40 @@ class App(QtCore.QObject):
         else:
         else:
             raise unknownException
             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):
     def raise_tcl_error(self, text):
         """
         """
         this method  pass exception from python into TCL as error, so we get stacktrace and reason
         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
         :return: raise exception
         """
         """
 
 
-        self.tcl.eval('return -code error "%s"' % text)
+        self.display_tcl_error(text)
         raise self.TclErrorException(text)
         raise self.TclErrorException(text)
 
 
     def exec_command(self, 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
         # so we actually are sorting the tools by diameter
         sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
         sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1])
         if tools == "all":
         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))
             log.debug("Tools 'all' and sorted are: %s" % str(tools))
         else:
         else:
             selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
             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:
         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 += t % (0, 0)
         gcode += "M05\n"  # Spindle stop
         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():
         for key, value in self.help['args'].items():
             help_string.append(get_decorated_argument(key, value))
             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']:
         for example in self.help['examples']:
             help_string.append(get_decorated_example(example))
             help_string.append(get_decorated_example(example))
 
 
@@ -192,10 +196,13 @@ class TclCommand(object):
 
 
         # check options
         # check options
         for key in 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)
                 self.raise_tcl_error('Unknown parameter: %s' % key)
             try:
             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:
             except Exception, e:
                 self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
                 self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
                                      % (key, self.option_types[key], str(e)))
                                      % (key, self.option_types[key], str(e)))
@@ -207,6 +214,31 @@ class TclCommand(object):
 
 
         return named_args, unnamed_args
         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):
     def execute_wrapper(self, *args):
         """
         """
         Command which is called by tcl console when current commands aliases are hit.
         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)
             args, unnamed_args = self.check_args(args)
             return self.execute(args, unnamed_args)
             return self.execute(args, unnamed_args)
         except Exception as unknown:
         except Exception as unknown:
+            error_info=sys.exc_info()
             self.log.error("TCL command '%s' failed." % str(self))
             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
     @abc.abstractmethod
     def execute(self, args, unnamed_args):
     def execute(self, args, unnamed_args):
@@ -242,7 +276,6 @@ class TclCommand(object):
 
 
         raise NotImplementedError("Please Implement this method")
         raise NotImplementedError("Please Implement this method")
 
 
-
 class TclCommandSignaled(TclCommand):
 class TclCommandSignaled(TclCommand):
     """
     """
         !!! I left it here only  for demonstration !!!
         !!! I left it here only  for demonstration !!!
@@ -258,15 +291,18 @@ class TclCommandSignaled(TclCommand):
         it handles  all neccessary stuff about blocking and passing exeptions
         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
     output = None
 
 
     def execute_call(self, args, unnamed_args):
     def execute_call(self, args, unnamed_args):
 
 
         try:
         try:
+            self.output = None
+            self.error=None
+            self.error_info=None
             self.output = self.execute(args, unnamed_args)
             self.output = self.execute(args, unnamed_args)
+        except Exception as unknown:
+            self.error_info = sys.exc_info()
+            self.error=unknown
         finally:
         finally:
             self.app.shell_command_finished.emit(self)
             self.app.shell_command_finished.emit(self)
 
 
@@ -281,7 +317,7 @@ class TclCommandSignaled(TclCommand):
         """
         """
 
 
         @contextmanager
         @contextmanager
-        def wait_signal(signal, timeout=10000):
+        def wait_signal(signal, timeout=300000):
             """Block loop until signal emitted, or timeout (ms) elapses."""
             """Block loop until signal emitted, or timeout (ms) elapses."""
             loop = QtCore.QEventLoop()
             loop = QtCore.QEventLoop()
 
 
@@ -318,10 +354,10 @@ class TclCommandSignaled(TclCommand):
             # Restore exception management
             # Restore exception management
             sys.excepthook = oeh
             sys.excepthook = oeh
             if ex:
             if ex:
-                self.raise_tcl_error(str(ex[0]))
+                raise ex[0]
 
 
             if status['timed_out']:
             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:
         try:
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
@@ -331,17 +367,15 @@ class TclCommandSignaled(TclCommand):
                 passed_timeout=args['timeout']
                 passed_timeout=args['timeout']
                 del args['timeout']
                 del args['timeout']
             else:
             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
             # set detail for processing, it will be there until next open or close
             self.app.shell.open_proccessing(self.get_current_command())
             self.app.shell.open_proccessing(self.get_current_command())
 
 
-            self.output = None
-
             def handle_finished(obj):
             def handle_finished(obj):
                 self.app.shell_command_finished.disconnect(handle_finished)
                 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)
             self.app.shell_command_finished.connect(handle_finished)
 
 
@@ -354,5 +388,7 @@ class TclCommandSignaled(TclCommand):
             return self.output
             return self.output
 
 
         except Exception as unknown:
         except Exception as unknown:
+            error_info=sys.exc_info()
             self.log.error("TCL command '%s' failed." % str(self))
             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).'),
             ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
             ('multidepth', 'Use or not multidepth cnccut.'),
             ('multidepth', 'Use or not multidepth cnccut.'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('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': []
         '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 pkgutil
 import sys
 import sys
 
 
-# allowed command modules
+# allowed command modules (please append them alphabetically ordered)
 import tclCommands.TclCommandAddPolygon
 import tclCommands.TclCommandAddPolygon
 import tclCommands.TclCommandAddPolyline
 import tclCommands.TclCommandAddPolyline
 import tclCommands.TclCommandCncjob
 import tclCommands.TclCommandCncjob
+import tclCommands.TclCommandDrillcncjob
 import tclCommands.TclCommandExportGcode
 import tclCommands.TclCommandExportGcode
 import tclCommands.TclCommandExteriors
 import tclCommands.TclCommandExteriors
 import tclCommands.TclCommandInteriors
 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)
     module = loader.find_module(name).load_module(name)
     __all__.append(name)
     __all__.append(name)
 
 
-
 def register_all_commands(app, commands):
 def register_all_commands(app, commands):
     """
     """
     Static method which register all known commands.
     Static method which register all known commands.