diff --git a/docs/features/action_commands.rst b/docs/features/action_commands.rst new file mode 100644 index 00000000..82591cf9 --- /dev/null +++ b/docs/features/action_commands.rst @@ -0,0 +1,36 @@ +.. _sec-features-action_commands: + +Action Commands +=============== + +Action commands are a feature defined for the GCODE based RepRap communication protocol. To quote from the +`GCODE node of the RepRap wiki `_: + + The RepRap machine may also send lines that look like this: + + **// This is some debugging or other information on a line on its own. It may be sent at any time.** + + Such lines will always be preceded by //. + + On the latest version of Pronterface and [...] OctoPrint a special comment of the form: + + **// action:command** + + is allowed to be sent from the firmware[. T]he command can currently be pause, resume or disconnect which will + execute those commands on the host. As this is also a comment other hosts will just ignored these commands. + +OctoPrint out of the box supports handling of the above mentioned commands: + +pause + When this command is received from the printer, OctoPrint will pause streaming of a current print job just like if the + "Pause" button had been clicked. + +resume + When this command is received from the printer, OctoPrint will resume streaming of a current print job just like if + the "Resume" button had been clicked. + +disconnect + When this command is Received from the printer, OctoPrint will immediately disconnect from it. + +Support for additional commands may be added by plugins by implementing a handler for the +:ref:`octoprint.comm.protocol.action ` hook. diff --git a/docs/features/index.rst b/docs/features/index.rst index 83088990..29015bf6 100644 --- a/docs/features/index.rst +++ b/docs/features/index.rst @@ -9,3 +9,4 @@ Features custom_controls.rst gcode_scripts.rst + action_commands.rst diff --git a/docs/plugins/concepts.rst b/docs/plugins/concepts.rst new file mode 100644 index 00000000..9c733c1c --- /dev/null +++ b/docs/plugins/concepts.rst @@ -0,0 +1,196 @@ +.. _sec-plugin-concepts: + +Concepts +======== + +OctoPrint's plugins are `Python Packages `_ which in their +top-level module define a bunch of :ref:`control properties ` defining +metadata (like name, version etc of the plugin) as well as information on how to initialize the plugin and into what +parts of the system the plugin will actually plug in to perform its job. + +There are three types of ways a plugin might attach itself to the system, through so called +:ref:`mixin ` implementations, by attaching itself to specified +:ref:`hook ` or by offering :ref:`helper ` functionality to be +used by other plugins. + +.. _sec-plugin-concepts-mixins: + +Mixins +------ + +Plugin mixins are the heart of OctoPrint's plugin system. They are :ref:`special base classes ` +which are to be subclassed and extended to add functionality to OctoPrint. Plugins declare their instances that +implement one or multiple mixins using the ``__plugin_implementations__`` control property. OctoPrint's plugin core +collects those from the plugins and offers methods to access them based on the mixin type, which get used at multiple +locations within OctoPrint. + +Using mixins always follows the pattern of retrieving the matching implementations from the plugin subsystem, then +calling the specific mixin's methods as defined and necessary. + +The following snippet taken from OctoPrint's code for example shows how all :class:`~octoprint.plugin.AssetPlugin` +implementations are collected and then all assets they return via their ``get_assets`` methods are retrieved and +merged into one big asset map (differing between javascripts and stylesheets of various types) for use during +rendition of the UI. + +.. code-block:: python + :linenos: + + asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin) + for name, implementation in asset_plugins.items(): + all_assets = implementation.get_assets() + + if "js" in all_assets: + for asset in all_assets["js"]: + assets["js"].append(url_for('plugin_assets', name=name, filename=asset)) + + if preferred_stylesheet in all_assets: + for asset in all_assets[preferred_stylesheet]: + assets["stylesheets"].append((preferred_stylesheet, url_for('plugin_assets', name=name, filename=asset))) + else: + for stylesheet in supported_stylesheets: + if not stylesheet in all_assets: + continue + + for asset in all_assets[stylesheet]: + assets["stylesheets"].append((stylesheet, url_for('plugin_assets', name=name, filename=asset))) + break + +.. seealso:: + + :ref:`Available Mixins ` + An overview of all mixin types available for extending OctoPrint. + + :ref:`The Getting Started Guide ` + Tutorial on how to write a simple OctoPrint module utilizing mixins for various types of extension. + +.. _sec-plugin-concepts-hooks: + +Hooks +----- + +Hooks are the smaller siblings of mixins, allowing to extend functionality or data processing where a custom mixin type +would be too much overhead. Where mixins are based on classes, hooks are based on methods. Like with the mixin +implementations, plugins inform OctoPrint about hook handlers using a control property, ``__plugin_hooks__``. + +Each hook defines a contract detailing the call parameters for the hook handler method and the expected return type. +OctoPrint will call the hook with the define parameters and process the result depending on the hook. + +An example for a hook within OctoPrint is ``octoprint.comm.protocol.scripts``, which allows adding additional +lines to OctoPrint's :ref:`GCODE scripts `, either as ``prefix`` (before the existing lines) +or as ``postfix`` (after the existing lines). + +.. code-block:: python + :linenos: + + self._gcode_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.scripts") + + # ... + + for hook in self._gcodescript_hooks: + try: + retval = self._gcodescript_hooks[hook](self, "gcode", scriptName) + except: + self._logger.exception("Error while processing gcodescript hook %s" % hook) + else: + if retval is None: + continue + if not isinstance(retval, (list, tuple)) or not len(retval) == 2: + continue + + def to_list(data): + if isinstance(data, str): + data = map(str.strip, data.split("\n")) + elif isinstance(data, unicode): + data = map(unicode.strip, data.split("\n")) + + if isinstance(data, (list, tuple)): + return list(data) + else: + return None + + prefix, suffix = map(to_list, retval) + if prefix: + scriptLines = list(prefix) + scriptLines + if suffix: + scriptLines += list(suffix) + +As you can see, the hook's method signature is defined to take the current ``self`` (as in, the current comm layer instance), +the general type of script for which to look for additions ("gcode") and the script name for which to look (e.g. +``beforePrintStarted`` for the GCODE script executed before the beginning of a print job). The hook is expected to +return a 2-tuple of prefix and postfix if has something for either of those, otherwise ``None``. OctoPrint will then take +care to add prefix and suffix as necessary after a small round of preprocessing. + +.. note:: + + At the moment there exists no way to determine the execution order of various hook handlers within OctoPrint, + or to prevent the execution of further handlers down the chain. + + This is planned for the very near future though. + +Plugins can easily add their own hooks too. For example, the `Software Update Plugin `_ +declares a custom hook "octoprint.plugin.softwareupdate.check_config" which other plugins can add handlers for in order +to register themselves with the Software Update Plugin by returning their own update check configuration. + +If you want your hook handler to be an instance method of a mixin implementation of your plugin (for example since you +need access to instance variables handed to your implementation via mixin invocations), you can get this work +by using a small trick. Instead of defining it directly via ``__plugin_hooks__`` utilize the ``__plugin_init__`` +property instead, manually instantiate your implementation instance and then add its hook handler method to the +``__plugin_hooks__`` property and itself to the ``__plugin_implementations__`` property. Example: + +.. code-block:: python + :linenos: + + # ... + + class MyPluginImplementation(octoprint.plugin.StartupPlugin): + + def __init__(self): + self._host = None + self._port = None + + def on_startup(self, host, port): + self._host = host + self._port = port + + def my_script_hook_handler(self, comm, script_type, script_name): + if not script_type == "gcode" or not script_name == "afterPrinterConnected": + # we only extend the gcode script afterPrinterConnected, ignore this + return None + + if not self._host or not self._port: + # oops, we haven't started up yet, shouldn't even happen... + return None + + # we only append something to the existing script, we don't prepend + prefix = None + postfix = ["M117 Hello World from %s:%d" % (self._host, self._port)] + return prefix, postfix + + # ... + + __plugin_implementations__ = [] + __plugin_hooks__ = dict() + + def __plugin_init__(): + # this gets called when OctoPrint initializes the plugin, so let's create the + # instance of our main implementation ... + plugin = MyPluginImplementation() + + # ... register it as implementation ... + global __plugin_implementations__ + __plugin_implementations__ = [plugin] + + # ... and its hook handler method as hook handler. + global __plugin_hooks__ + __plugin_hooks__ = {"octoprint.comm.protocol.scripts": plugin.my_script_hook_handler} + +.. seealso:: + + :ref:`Available Hooks ` + An overview of all hooks defined in OctoPrint itself. + + +.. _sec-plugin-concepts-helpers: + +Helpers +------- diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 1b15dd4a..6843c384 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -3,6 +3,65 @@ Available plugin hooks ====================== -.. todo:: +.. _sec-plugins-hook-comm-protocol-gcode: - Needs to be documented \ No newline at end of file +octoprint.comm.protocol.gcode +----------------------------- + +.. _sec-plugins-hook-comm-protocol-action: + +octoprint.comm.protocol.action +------------------------------ + +.. py:function:: hook(comm_instance, line, action) + + React to a :ref:`action command <>` received from the printer. + + Hook handlers may use this to react to react to custom firmware messages. OctoPrint parses the received action + command ``line`` and provides the parsed ``action`` (so anything after ``// action:``) to the hook handler. + + No returned value is expected. + + :param object comm_instance: The :class:`~octoprint.util.comm.MachineCom` instance which triggered the hook. + :param str line: The complete line as received from the printer, format ``// action:`` + :param str action: The parsed out action command, so for a ``line`` like ``// action:some_command`` this will be + ``some_command`` + +.. _sec-plugins-hook-comm-protocol-scripts: + +octoprint.comm.protocol.scripts +------------------------------- + +.. py:function:: hook(comm_instance, script_type, script_name) + + 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 + handled scripts. If not, None should be returned directly. + + If the hook handler has something to add to the specified script, it may return a 2-tuple, with the first entry + defining the prefix (what to *prepend* to the script in question) and the last entry defining the postfix (what to + *append* to the script in question). Both prefix and postfix can be None to signify that nothing should be prepended + respectively appended. + + The returned entries may be either iterables of script lines or a string including newlines of the script lines (which + will be split by the caller if necessary). + + Example: + + .. code-block:: python + + def hook(comm_instance, script_type, script_name): + if not script_type == "gcode" or not script_name == "afterPrinterConnected": + return None + + prefix = "M117 Hello\nM117 Hello World" + postfix = ["M117 Connected", "M117 to OctoPrint"] + return prefix, postfix + + __plugin_hooks__ = {"octoprint.comm.protocol.scripts": hook} + + :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. + :param str script_name: The name of the script for which the hook was called. + :return: A 2-tuple in the form ``(prefix, postfix)`` or None + :rtype: tuple or None \ No newline at end of file diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index c043aaf6..74d23be2 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -15,6 +15,7 @@ additional slicers. More plugin types are planned for the future. :maxdepth: 3 using.rst + concepts.rst gettingstarted.rst infrastructure.rst templates.rst