diff --git a/AUTHORS.md b/AUTHORS.md index bea57d2e..a56725a7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -80,6 +80,7 @@ date of first contribution): * [Andreas Werner](https://github.com/gallore) * [Shawn Bruce](https://github.com/kantlivelong) * [Claudiu Ceia] (https://github.com/ClaudiuCeia) + * [Goswin von Brederlow](https://github.com/mrvn) OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by [Daid Braam](https://github.com/daid). Parts of its communication layer and diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index cfdf3800..0e0811e9 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -450,6 +450,12 @@ This describes actually four hooks: should use this option. * A 2-tuple consisting of a rewritten version of the ``cmd`` and the ``cmd_type``, e.g. ``return "M105", "temperature_poll"``. Handlers which wish to rewrite both the command and the command type should use this option. + * Queuing only: A list of strings replacing the command with a series of commands of the same ``cmd_type``. + * Queuing only: A list of 2-tuple replacing the command with a series of commands with new ``cmd_type``. This allows + replacing a command with a series of commands of different type. + + Note: Only one command of a given ``cmd_type`` (other than None) may be queued at a time. Trying to rewrite the ``cmd_type`` + to one already in the queue will give an error. **Example** diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 39f792cf..0844b3e8 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -2020,23 +2020,34 @@ class MachineCom(object): gcode = None - # trigger the "queuing" phase only if we are not streaming to sd right now - cmd, cmd_type, gcode = self._process_command_phase("queuing", cmd, cmd_type, gcode=gcode) + if not self.isStreaming(): + # trigger the "queuing" phase only if we are not streaming to sd right now + results = self._process_command_phase("queuing", cmd, cmd_type, gcode=gcode) - if cmd is None: - # command is no more, return - return False - - if not self.isStreaming() and gcode and gcode in gcodeToEvent: - # if this is a gcode bound to an event, trigger that now - eventManager().fire(gcodeToEvent[gcode]) - - # actually enqueue the command for sending - if self._enqueue_for_sending(cmd, command_type=cmd_type, on_sent=on_sent): - self._process_command_phase("queued", cmd, cmd_type, gcode=gcode) - return True + if not results: + # commnd is no more, return + return False else: - return False + results = [(cmd, cmd_type, gcode)] + + sent_something = False + for (cmd, cmd_type, gcode) in results: + if cmd is None: + # no command, next entry + continue + + if gcode and gcode in gcodeToEvent: + # if this is a gcode bound to an event, trigger that now + eventManager().fire(gcodeToEvent[gcode]) + + # actually enqueue the command for sending + if self._enqueue_for_sending(cmd, command_type=cmd_type, on_sent=on_sent): + if not self.isStreaming(): + # trigger the "queued" phase only if we are not streaming to sd right now + self._process_command_phase("queued", cmd, cmd_type, gcode=gcode) + sent_something = True + + return sent_something ##~~ send loop handling @@ -2096,9 +2107,9 @@ class MachineCom(object): else: # trigger "sending" phase - command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode) + results = self._process_command_phase("sending", command, command_type, gcode=gcode) - if command is None: + if results == []: # No, we are not going to send this, that was a last-minute bail. # However, since we already are in the send queue, our _monitor # loop won't be triggered with the reply from this unsent command @@ -2109,6 +2120,10 @@ class MachineCom(object): # and now let's fetch the next item from the queue continue + # no command expansion allowed in sending, use queuing instead + assert(len(results) == 1) + (command, _, gcode) = results[0] + if command.strip() == "": self._logger.info("Refusing to send an empty line to the printer") @@ -2170,61 +2185,94 @@ class MachineCom(object): if gcode is None: gcode = gcode_command_for_cmd(command) + results = [(command, command_type, gcode)] + + if phase not in ("queuing", "queued", "sending", "sent"): + return results + # send it through the phase specific handlers provided by plugins for name, hook in self._gcode_hooks[phase].items(): - try: - hook_result = hook(self, phase, command, command_type, gcode) - except: - self._logger.exception("Error while processing hook {name} for phase {phase} and command {command}:".format(**locals())) - else: - command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, hook_result) - if command is None: - # hook handler return None as command, so we'll stop here and return a full out None result - return None, None, None + new_results = [] + for (command, command_type, gcode) in results: + try: + hook_results = hook(self, phase, command, command_type, gcode) + except: + self._logger.exception("Error while processing hook {name} for phase {phase} and command {command}:".format(**locals())) + else: + new_results += self._handle_command_handler_result(command, command_type, gcode, hook_results) + if new_results == []: + # hook handler returned None for all commands, so we'll stop here and return a full out None result + return [] + results = new_results # if it's a gcode command send it through the specific handler if it exists - if gcode is not None: - gcodeHandler = "_gcode_" + gcode + "_" + phase - if hasattr(self, gcodeHandler): - handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type) - command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result) + new_results = [] + for (command, command_type, gcode) in results: + if gcode is not None: + gcodeHandler = "_gcode_" + gcode + "_" + phase + if hasattr(self, gcodeHandler): + handler_results = getattr(self, gcodeHandler)(command, cmd_type=command_type) + new_results += self._handle_command_handler_result(command, command_type, gcode, handler_results) + else: + new_results.append((command, command_type, gcode)) + if new_results == []: + # gcode handler returned None for all commands, so we'll stop here and return a full out None result + return [] + results = new_results # send it through the phase specific command handler if it exists commandPhaseHandler = "_command_phase_" + phase if hasattr(self, commandPhaseHandler): - handler_result = getattr(self, commandPhaseHandler)(command, cmd_type=command_type, gcode=gcode) - command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result) + new_results = [] + for (command, command_type, gcode) in results: + handler_results = getattr(self, commandPhaseHandler)(command, cmd_type=command_type, gcode=gcode) + new_results += self._handle_command_handler_result(command, command_type, gcode, handler_results) + results = new_results # finally return whatever we resulted on - return command, command_type, gcode + return results - def _handle_command_handler_result(self, command, command_type, gcode, handler_result): - original_tuple = (command, command_type, gcode) + def _handle_command_handler_result(self, command, command_type, gcode, handler_results): + default = [(command, command_type, gcode)] - if handler_result is None: + if handler_results is None: # handler didn't return anything, we'll just continue - return original_tuple + return default - if isinstance(handler_result, basestring): - # handler did return just a string, we'll turn that into a 1-tuple now - handler_result = (handler_result,) - elif not isinstance(handler_result, (tuple, list)): - # handler didn't return an expected result format, we'll just ignore it and continue - return original_tuple + if isinstance(handler_results, basestring): + # handler did return just a string, replace command with it + command = handler_result + gcode = gcode_command_for_cmd(command) + return [(command, command_type, gcode)] + + elif isinstance(handler_results, tuple): + # handler did return a tuple, extract command and command_type + hook_result_length = len(handler_results) + if hook_result_length == 1: + # handler returned just the command + command, = handler_results + elif hook_result_length == 2: + # handler returned command and command_type + command, command_type = handler_results + else: + # handler returned a tuple of an unexpected length + return default + + gcode = gcode_command_for_cmd(command) + return [(command, command_type, gcode)] + + elif isinstance(handler_results, list): + # handler did return a list of commands or commands + command_type tuples + if isinstance(handler_results[0], tuple): + # handler returned command and command_type + return [(command, command_type, gcode_command_for_cmd(command)) for (command, command_type) in handler_results] + else: + # handler returned just the commands + return [(command, command_type, gcode_command_for_cmd(command)) for command in handler_results] - hook_result_length = len(handler_result) - if hook_result_length == 1: - # handler returned just the command - command, = handler_result - elif hook_result_length == 2: - # handler returned command and command_type - command, command_type = handler_result else: - # handler returned a tuple of an unexpected length - return original_tuple - - gcode = gcode_command_for_cmd(command) - return command, command_type, gcode + # handler didn't return an expected result format, we'll just ignore it and continue + return default ##~~ actual sending via serial