From c6e4057adde5f4a8327f120ff80d9de31ed1d73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 25 Mar 2015 18:24:51 +0100 Subject: [PATCH] [Docs] Documented octoprint.comm.protocol.gcode hook Experimenting with including examples stored on github, let's see if RTD likes that. --- docs/conf.py | 3 +- docs/plugins/hooks.rst | 51 +++++++++++++++++++++++++++--- docs/sphinxext/onlineinclude.py | 56 +++++++++++++++++++++++++++++++++ src/octoprint/util/comm.py | 42 +++++++++++++++++++++++-- 4 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 docs/sphinxext/onlineinclude.py diff --git a/docs/conf.py b/docs/conf.py index 10688b0b..12968c0d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,7 +32,8 @@ needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['codeblockext', 'sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain', 'sphinx.ext.napoleon'] +extensions = ['codeblockext', 'onlineinclude', 'sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain', + 'sphinx.ext.napoleon'] todo_include_todos = True # Add any paths that contain templates here, relative to this directory. diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 6843c384..f89931f9 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -8,12 +8,54 @@ Available plugin hooks octoprint.comm.protocol.gcode ----------------------------- +.. py:function:: hook(comm_instance, cmd, cmd_type=None, with_checksum=None) + + Preprocess and optionally suppress a GCODE command before it is being sent to the printer. + + Hook handlers may use this to rewrite or completely suppress certain commands before they enter the send queue of + the communication layer. The hook handler will be called with the ``cmd`` to be sent to the printer as well as + the parameters for sending like ``cmd_type`` and ``with_checksum``. + + If the handler does not wish to handle the command, it should simply perform a ``return cmd`` as early as possible, + that will ensure that no changes are applied to the command. + + If the handler wishes to suppress sending of the command altogether, it should return None instead. That will tell + OctoPrint that the ``cmd`` has been scraped altogether and not send anything. + + More granular manipulation of the sending logic is possible by not just returning ``cmd`` (be it the original, a + rewritten variant or a None value) but a 2-tuple (cmd, cmd_type) or a 3-tuple (cmd, cmd_type, with_checksum). This + allows to also rewrite the ``cmd_type`` and the ``with_checksum`` parameter used for sending. Note that the latter + should only be necessary in very rare circumstances, since usually plugins should not need to have to decide whether + a command should be sent with a checksum or not. + + Defining a ``cmd_type`` other than None will make sure OctoPrint takes care of only having one command of that type + in its sending queue. Predefined types are ``temperature_poll`` for temperature polling via ``M105`` and + ``sd_status_poll`` for polling the SD printing status via ``M27``. + + **Example** + + The following hook handler replaces all ``M107`` ("Fan Off", deprecated) with an ``M106 S0`` ("Fan On" with speed + parameter) + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/rewrite_m107.py + :linenos: + :lines: 3- + + :param object comm_instance: The :class:`~octoprint.util.comm.MachineCom` instance which triggered the hook. + :param str cmd: The GCODE command for which the hook was triggered. This is the full command as taken either + from the currently streamed GCODE file or via other means (e.g. user input our status polling). + :param str cmd_type: Type of command, ``temperature_poll`` for temperature polling or ``sd_status_poll`` for SD + printing status polling. + :param boolean with_checksum: Whether the ``cmd`` was to be sent with a checksum (True) or not (False) + :return: A rewritten ``cmd``, a tuple of ``cmd`` and ``cmd_type`` or ``cmd``, ``cmd_type`` and ``with_checksum`` + or None to suppress sending of the ``cmd`` to the printer. See above for details. + .. _sec-plugins-hook-comm-protocol-action: octoprint.comm.protocol.action ------------------------------ -.. py:function:: hook(comm_instance, line, action) +.. py:function:: hook(comm_instance, line, action, *args, **kwargs) React to a :ref:`action command <>` received from the printer. @@ -32,7 +74,7 @@ octoprint.comm.protocol.action octoprint.comm.protocol.scripts ------------------------------- -.. py:function:: hook(comm_instance, script_type, script_name) +.. py:function:: hook(comm_instance, script_type, script_name, *args, **kwargs) Return a prefix to prepend and a postfix to append to the script ``script_name`` of type ``type``. Handlers should make sure to only proceed with returning additional scripts if the ``script_type`` and ``script_name`` match @@ -50,7 +92,7 @@ octoprint.comm.protocol.scripts .. code-block:: python - def hook(comm_instance, script_type, script_name): + def handler(comm_instance, script_type, script_name): if not script_type == "gcode" or not script_name == "afterPrinterConnected": return None @@ -58,7 +100,8 @@ octoprint.comm.protocol.scripts postfix = ["M117 Connected", "M117 to OctoPrint"] return prefix, postfix - __plugin_hooks__ = {"octoprint.comm.protocol.scripts": hook} + __plugin_name__ = "Example: GCODE script hook demo" + __plugin_hooks__ = {"octoprint.comm.protocol.scripts": handler} :param MachineCom comm_instance: The :class:`~octoprint.util.comm.MachineCom` instance which triggered the hook. :param str script_type: The type of the script for which the hook was called, currently only "gcode" is supported here. diff --git a/docs/sphinxext/onlineinclude.py b/docs/sphinxext/onlineinclude.py new file mode 100644 index 00000000..633eeef3 --- /dev/null +++ b/docs/sphinxext/onlineinclude.py @@ -0,0 +1,56 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'The MIT License ' +__copyright__ = "Copyright (C) 2015 Gina Häußge - Released under terms of the MIT License" + + +import codecs +import urllib2 + +from sphinx.directives import LiteralInclude, dedent_lines + +cache = dict() + +class OnlineIncludeDirective(LiteralInclude): + + def read_with_encoding(self, filename, document, codec_info, encoding): + global cache + + f = None + try: + if not self.arguments[0] in cache: + f = codecs.StreamReaderWriter(urllib2.urlopen(self.arguments[0]), codec_info[2], + codec_info[3], 'strict') + lines = f.readlines() + cache[self.arguments[0]] = lines + else: + lines = cache[self.arguments[0]] + + lines = dedent_lines(lines, self.options.get('dedent')) + return lines + except (IOError, OSError, urllib2.URLError): + return [document.reporter.warning( + 'Include file %r not found or reading it failed' % self.arguments[0], + line=self.lineno)] + except UnicodeError: + return [document.reporter.warning( + 'Encoding %r used for reading included file %r seems to ' + 'be wrong, try giving an :encoding: option' % + (encoding, self.arguments[0]))] + finally: + if f is not None: + f.close() + +def visit_onlineinclude(translator, node): + translator.visit_literal_block(node) + +def depart_onlineinclude(translator, node): + translator.depart_literal_block(node) + +def setup(app): + app.add_directive("onlineinclude", OnlineIncludeDirective) + + handler = (visit_onlineinclude, depart_onlineinclude) + app.add_node(OnlineIncludeDirective, html=handler, latex=handler, text=handler) \ No newline at end of file diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 1b88c9a7..ab8cbea4 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -1362,16 +1362,35 @@ class MachineCom(object): if not self.isStreaming(): for hook in self._gcode_hooks: - hook_cmd = self._gcode_hooks[hook](self, cmd) - if hook_cmd and isinstance(hook_cmd, basestring): + hook_cmd = self._gcode_hooks[hook](self, cmd, cmd_type=cmd_type, send_checksum=sendChecksum) + + # hook might have returned (cmd, sendChecksum) or (cmd, sendChecksum, cmd_type), split that + if isinstance(hook_cmd, tuple): + if len(hook_cmd) == 2: + hook_cmd, cmd_type = hook_cmd + elif len(hook_cmd) == 3: + hook_cmd, cmd_type, sendChecksum = hook_cmd + + # hook might have returned None for cmd, if so stop processing, we won't send this command + # to the printer + if hook_cmd is None: + cmd = None + break + + # if hook_cmd is a string, we'll replace cmd with it (it's been rewritten by the hook handler + elif isinstance(hook_cmd, basestring): cmd = hook_cmd + + # try to parse the cmd and extract the gcode type gcode = self._regex_command.search(cmd) if gcode: gcode = gcode.group(1) + # fire events if necessary if gcode in gcodeToEvent: eventManager().fire(gcodeToEvent[gcode]) + # send it through the specific handler if it exists gcodeHandler = "_gcode_" + gcode if hasattr(self, gcodeHandler): cmd = getattr(self, gcodeHandler)(cmd) @@ -1388,6 +1407,25 @@ class MachineCom(object): else: self._enqueue_for_sending(cmd, command_type=cmd_type) + def gcode_command_for_cmd(self, cmd): + """ + Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0"). + + Arguments: + cmd (str): The command to try to parse. + + Returns: + str or None: The GCODE command identifier if it could be parsed, or None if not. + """ + if not cmd: + return None + + gcode = self._regex_command.search(cmd) + if not gcode: + return None + + return gcode.group(1) + ##~~ send loop handling def _enqueue_for_sending(self, command, linenumber=None, command_type=None):