diff --git a/AUTHORS.md b/AUTHORS.md index 0309fdf5..412f9077 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -53,6 +53,8 @@ date of first contribution): * [Andrew Erickson](https://github.com/aerickson) * [Nicanor Romero Venier](https://github.com/nicanor-romero) * [Thomas Hou](https://github.com/masterhou) + * [Mark Bastiaans](https://github.com/markbastiaans) + * [Marcel Hellwig](https://github.com/punkkeks) 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/CHANGELOG.md b/CHANGELOG.md index 7b19a999..1cf331cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ * It's not possible anymore to select files that are not machinecode files (e.g. GCODE) for printing on the file API. * Changes to a user's personal settings via the UI now propagate across sessions. +* [#1047](https://github.com/foosel/OctoPrint/issues/1047) - Fixed 90 degree + webcam rotation for iOS Safari. ## 1.2.6 (2015-09-02) diff --git a/docs/api/job.rst b/docs/api/job.rst index 3bcc1fde..eeb3b071 100644 --- a/docs/api/job.rst +++ b/docs/api/job.rst @@ -37,7 +37,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -54,7 +54,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -71,7 +71,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... @@ -88,7 +88,7 @@ Issue a job command .. sourcecode:: http - POST /api/control/job HTTP/1.1 + POST /api/job HTTP/1.1 Host: example.com Content-Type: application/json X-Api-Key: abcdef... diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst index 36a6e2c3..add3d78b 100644 --- a/docs/plugins/hooks.rst +++ b/docs/plugins/hooks.rst @@ -159,6 +159,32 @@ This describes actually four hooks: :param str gcode: Parsed GCODE command, e.g. ``G0`` or ``M110``, may also be None if no known command could be parsed :return: None, 1-tuple, 2-tuple or string, see the description above for details. +.. _sec-plugins-hook-comm-protocol-gcode-received: + +octoprint.comm.protocol.gcode.received +-------------------------------------- + +.. py:function:: hook(comm_instance, line, *args, **kwargs) + + Get the returned lines sent by the printer. Handlers should return the received line or in any case, the modified + version of it. If the the handler returns None, processing will be aborted and the communication layer will get an + empty string as the received line. Note that Python functions will also automatically return ``None`` if an empty + ``return`` statement is used or just nothing is returned explicitely from the handler. + + **Example:** + + Looks for the response of a M115, which contains information about the MACHINE_TYPE, among other things. + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/read_m115_response.py + :linenos: + :tab-width: 4 + :caption: `read_m115_response.py `_ + + :param MachineCom comm_instance: The :class:`~octoprint.util.comm.MachineCom` instance which triggered the hook. + :param str line: The line received from the printer. + :return: The received line or in any case, a modified version of it. + :rtype: str + .. _sec-plugins-hook-comm-protocol-scripts: octoprint.comm.protocol.scripts diff --git a/docs/plugins/mixins.rst b/docs/plugins/mixins.rst index b57c012e..791d2aa3 100644 --- a/docs/plugins/mixins.rst +++ b/docs/plugins/mixins.rst @@ -18,6 +18,7 @@ StartupPlugin .. autoclass:: octoprint.plugin.StartupPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-shutdownplugin: @@ -26,6 +27,7 @@ ShutdownPlugin .. autoclass:: octoprint.plugin.ShutdownPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-settingsplugin: @@ -34,6 +36,7 @@ SettingsPlugin .. autoclass:: octoprint.plugin.SettingsPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-assetplugin: @@ -42,6 +45,7 @@ AssetPlugin .. autoclass:: octoprint.plugin.AssetPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-templateplugin: @@ -50,6 +54,7 @@ TemplatePlugin .. autoclass:: octoprint.plugin.TemplatePlugin :members: + :show-inheritance: .. _sec-plugins-mixins-wizardplugin: @@ -58,6 +63,16 @@ WizardPlugin .. autoclass:: octoprint.plugin.WizardPlugin :members: + :show-inheritance: + +.. _sec-plugins-mixins-uiplugin: + +UiPlugin +-------- + +.. autoclass:: octoprint.plugin.UiPlugin + :members: + :show-inheritance: .. _sec-plugins-mixins-simpleapiplugin: @@ -66,6 +81,7 @@ SimpleApiPlugin .. autoclass:: octoprint.plugin.SimpleApiPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-blueprintplugin: @@ -74,6 +90,7 @@ BlueprintPlugin .. autoclass:: octoprint.plugin.BlueprintPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-eventhandlerplugin: @@ -82,6 +99,7 @@ EventHandlerPlugin .. autoclass:: octoprint.plugin.EventHandlerPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-progressplugin: @@ -90,6 +108,7 @@ ProgressPlugin .. autoclass:: octoprint.plugin.ProgressPlugin :members: + :show-inheritance: .. _sec-plugins-mixins-slicerplugin: @@ -98,4 +117,5 @@ SlicerPlugin .. autoclass:: octoprint.plugin.SlicerPlugin :members: + :show-inheritance: diff --git a/run b/run index 0c51da39..1145a24f 100755 --- a/run +++ b/run @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 import os import sys diff --git a/setup.py b/setup.py index fc2a74d6..5130635b 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # coding=utf-8 from setuptools import setup, find_packages diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index 67baa4f7..883acc5d 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -1,4 +1,5 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 + import sys from octoprint.daemon import Daemon from octoprint.server import Server diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 1df93a9b..686c7836 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -104,7 +104,8 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en SlicerPlugin, AppPlugin, ProgressPlugin, - WizardPlugin] + WizardPlugin, + UiPlugin] if plugin_entry_points is None: plugin_entry_points = "octoprint.plugin" if plugin_disabled_list is None: diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 028ba436..30577575 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -12,6 +12,12 @@ way and could be extracted into a separate Python module in the future. .. autoclass:: Plugin :members: +.. autoclass:: RestartNeedingPlugin + :members: + +.. autoclass:: SortablePlugin + :members: + """ from __future__ import absolute_import @@ -1077,11 +1083,12 @@ class PluginManager(object): except: self.logger.exception("Error while trying to retrieve sorting order for plugin {}".format(impl[0])) - try: - int(sorting_value) - except ValueError: - self.logger.warn("The order value returned by {} for sorting context {} is not a valid integer, ignoring it".format(impl[0], sorting_context)) - sorting_value = None + if sorting_value is not None: + try: + int(sorting_value) + except ValueError: + self.logger.warn("The order value returned by {} for sorting context {} is not a valid integer, ignoring it".format(impl[0], sorting_context)) + sorting_value = None return sorting_value is None, sorting_value, impl[0] @@ -1279,10 +1286,34 @@ class Plugin(object): pass class RestartNeedingPlugin(Plugin): - pass + """ + Mixin for plugin types that need a restart in order to be enabled. + """ class SortablePlugin(Plugin): + """ + Mixin for plugin types that are sortable. + """ + def get_sorting_key(self, context=None): + """ + Returns the sorting key to use for the implementation in the specified ``context``. + + May return ``None`` if order is irrelevant. + + Implementations returning None will be ordered by plugin identifier + after all implementations which did return a sorting key value that was + not None sorted by that. + + Arguments: + context (str): The sorting context for which to provide the + sorting key value. + + Returns: + int or None: An integer signifying the sorting key value of the plugin + (sorting will be done ascending), or None if the implementation + doesn't care about calling order. + """ return None class PluginNeedsRestart(Exception): diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 9fdbe024..c7acb288 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -9,6 +9,9 @@ Please note that the plugin implementation types are documented in the section .. autoclass:: OctoPrintPlugin :show-inheritance: +.. autoclass:: ReloadNeedingPlugin + :show-inheritance: + """ from __future__ import absolute_import @@ -77,7 +80,7 @@ class OctoPrintPlugin(Plugin): and if not creating it before returning it. Injected by the plugin core system upon initialization of the implementation. - .. automethod:: get_plugin_data_folder + .. automethod:: get_plugin_data_folder """ def get_plugin_data_folder(self): @@ -97,12 +100,18 @@ class OctoPrintPlugin(Plugin): class ReloadNeedingPlugin(Plugin): - pass + """ + Mixin for plugin types that need a reload of the UI in order to become usable. + """ class StartupPlugin(OctoPrintPlugin, SortablePlugin): """ The ``StartupPlugin`` allows hooking into the startup of OctoPrint. It can be used to start up additional services on or just after the startup of the server. + + ``StartupPlugin`` is a :class:`~octoprint.plugin.core.SortablePlugin`. The + relevant sorting context for :meth:`on_startup` is ``StartupPlugin.on_startup``, + the one for :meth:`on_after_startup` will be ``StartupPlugin.on_after_startup``. """ def on_startup(self, host, port): @@ -132,6 +141,9 @@ class ShutdownPlugin(OctoPrintPlugin, SortablePlugin): The ``ShutdownPlugin`` allows hooking into the shutdown of OctoPrint. It's usually used in conjunction with the :class:`StartupPlugin` mixin, to cleanly shut down additional services again that where started by the :class:`StartupPlugin` part of the plugin. + + ``ShutdownPlugin`` is a :class:`~octoprint.plugin.core.SortablePlugin`. + The relevant sorting context will be ``ShutdownPlugin.on_shutdown``. """ def on_shutdown(self): @@ -149,6 +161,8 @@ class AssetPlugin(OctoPrintPlugin, RestartNeedingPlugin): A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used by templates injected through a :class:`TemplatePlugin`. + + ``AssetPlugin`` is a :class:`~octoprint.plugins.core.RestartNeedingPlugin`. """ def get_asset_folder(self): @@ -297,6 +311,8 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): responsibility to ensure that all core functionality is still maintained. Plugins can also add additional template types by implementing the :ref:`octoprint.ui.web.templatetypes ` hook. + + ``TemplatePlugin`` is a :class:`~octoprint.plugin.core.ReloadNeedingPlugin`. """ def get_template_configs(self): @@ -485,6 +501,167 @@ class TemplatePlugin(OctoPrintPlugin, ReloadNeedingPlugin): return os.path.join(self._basefolder, "templates") +class UiPlugin(OctoPrintPlugin, SortablePlugin): + """ + The ``UiPlugin`` mixin allows plugins to completely replace the UI served + by OctoPrint when requesting the main page hosted at `/`. + + OctoPrint will query whether your mixin implementation will handle a + provided request by calling :meth:`~octoprint.plugin.UiPlugin.will_handle_ui` with the Flask + `Request `_ object as + parameter. If you plugin returns `True` here, OctoPrint will next call + :meth:`~octoprint.plugin.UiPlugin.on_ui_render` with a couple of parameters like + - again - the Flask Request object and the render keyword arguments as + used by the default OctoPrint web interface. For more information see below. + + There are two methods used in order to allow for caching of the actual + response sent to the client. Whatever a plugin implementation returns + from the call to its :meth:`~octoprint.plugin.UiPlugin.on_ui_render` method + will be cached server side. The cache will be emptied in case of explicit + no-cache headers sent by the client, or if the ``_refresh`` query parameter + on the request exists and is set to ``true``. To prevent caching of the + response altogether, a plugin may set no-cache headers on the returned + response as well. + + ``UiPlugin`` is a :class:`~octoprint.plugin.core.SortablePlugin`. The + relevant sorting context when acting as a UiPlugin is ``UiPlugin.will_handle_ui``. + The first plugin to return ``True`` will be the one whose ui will be used, + no further calls to :meth:`~octoprint.plugin.UiPlugin.on_ui_render` will be performed. + + If implementations want to serve custom templates in the :meth:`~octoprint.plugin.UiPlugin.on_ui_render` + method it is recommended to also implement the :class:`~octoprint.plugin.TemplatePlugin` + mixin. + + **Example** + + What follows is a very simple example that renders a different (non functional and + only exemplary) UI if the requesting client has a UserAgent string hinting + at it being a mobile device: + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/dummy_mobile_ui/__init__.py + :linenos: + :tab-width: 4 + :caption: `dummy_mobile_ui/__init__.py `_ + + .. onlineinclude:: https://raw.githubusercontent.com/OctoPrint/Plugin-Examples/master/dummy_mobile_ui/templates/dummy_mobile_ui_index.jinja2 + :linenos: + :tab-width: 4 + :caption: `dummy_mobile_ui/templates/dummy_mobile_ui_index.jinja2 `_ + + Try installing the above plugin ``dummy_mobile_ui`` (also available in the + `plugin examples repository `_) + into your OctoPrint instance. If you access it from a regular desktop browser, + you should still see the default UI. However if you access it from a mobile + device (make sure to not have that request the desktop version of pages!) + you should see the very simple dummy page defined above. + """ + + def will_handle_ui(self, request): + """ + Called by OctoPrint to determine if the mixin implementation will be + able to handle the ``request`` provided as a parameter. + + Return ``True`` here to signal that your implementation will handle + the request and that the result of its :meth:`~octoprint.plugin.UiPlugin.on_ui_render` method + is what should be served to the user. + + Arguments: + request (flask.Request): A Flask `Request `_ + object. + + Returns: + bool: ``True`` if the the implementation will serve the request, + ``False`` otherwise. + """ + return False + + def on_ui_render(self, now, request, render_kwargs): + """ + Called by OctoPrint to retrieve the response to send to the client + for the ``request`` to ``/``. Only called if :meth:`~octoprint.plugin.UiPlugin.will_handle_ui` + returned ``True``. + + ``render_kwargs`` will be a dictionary (whose contents are cached) which + will contain the following key and value pairs (note that not all + key value pairs contained in the dictionary are listed here, only + those you should depend on as a plugin developer at the current time): + + .. list-table:: + :widths: 5 95 + + * - debug + - ``True`` if debug mode is enabled, ``False`` otherwise. + * - firstRun + - ``True`` if the server is being run for the first time (not + configured yet), ``False`` otherwise. + * - version + - OctoPrint's version information. This is a ``dict`` with the + following keys: + + .. list-table:: + :widths: 5 95 + + * - number + - The version number (e.g. ``x.y.z``) + * - branch + - The GIT branch from which the OctoPrint instance was built + (e.g. ``master``) + * - display + - The full human readable version string, including the + branch information (e.g. ``x.y.z (master branch)`` + + * - uiApiKey + - The UI API key to use for unauthorized API requests. This is + freshly generated on every server restart. + * - templates + - Template data to render in the UI. Will be a ``dict`` containing entries + for all known template types. + + The sub structure for each key will be as follows: + + .. list-table:: + :widths: 5 95 + + * - order + - A list of template names in the order they should appear + in the final rendered page + * - entries + - The template entry definitions to render. Depending on the + template type those are either 2-tuples of a name and a ``dict`` + or directly ``dicts`` with information regarding the + template to render. + + For the possible contents of the data ``dicts`` see the + :class:`~octoprint.plugin.TemplatePlugin` mixin. + + * - pluginNames + - A list of names of :class:`~octoprint.plugin.TemplatePlugin` + implementation that were enabled when creating the ``templates`` + value. + * - locales + - The locales for which there are translations available. + + On top of that all additional template variables as provided by :meth:`~octoprint.plugin.TemplatePlugin.get_template_vars` + will be contained in the dictionary as well. + + Arguments: + now (datetime.datetime): The datetime instance representing "now" + for this request, in case your plugin implementation needs this + information. + request (flask.Request): A Flask `Request `_ object. + render_kwargs (dict): The (cached) render keyword arguments that + would usually be provided to the core UI render function. + + Returns: + flask.Response: Should return a Flask `Response `_ + object that can be served to the requesting client directly. May be + created with ``flask.make_response`` combined with something like + ``flask.render_template``. + """ + + return None + + class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin): """ The ``WizardPlugin`` mixin allows plugins to report to OctoPrint whether @@ -546,6 +723,8 @@ class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin): def get_wizard_version(self): return 1 + + ``WizardPlugin`` is a :class:`~octoprint.plugin.core.ReloadNeedingPlugin`. """ def is_wizard_required(self): @@ -850,6 +1029,8 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin): flask.url_for("plugin.myblueprintplugin.myEcho") # will return "/plugin/myblueprintplugin/echo" + + ``BlueprintPlugin`` implements :class:`~octoprint.plugins.core.RestartNeedingPlugin`. """ @staticmethod @@ -874,6 +1055,24 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin): return f return decorator + @staticmethod + def errorhandler(code_or_exception): + """ + A decorator to mark errorhandlings methods in your BlueprintPlugin subclass. Works just the same as Flask's + own ``errorhandler`` decorator available on blueprints. + + See `the documentation for flask.Blueprint.errorhandler `_ + and `the documentation for flask.Flask.errorhandler `_ for more + information. + """ + from collections import defaultdict + def decorator(f): + if not hasattr(f, "_blueprint_error_handler") or f._blueprint_error_handler is None: + f._blueprint_error_handler = defaultdict(list) + f._blueprint_error_handler[f.__name__].append(code_or_exception) + return f + return decorator + def get_blueprint(self): """ Creates and returns the blueprint for your plugin. Override this if you want to define and handle your blueprint yourself. @@ -892,6 +1091,9 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin): for blueprint_rule in f._blueprint_rules[member]: rule, options = blueprint_rule blueprint.add_url_rule(rule, options.pop("endpoint", f.__name__), view_func=f, **options) + if hasattr(f, "_blueprint_error_handler") and member in f._blueprint_error_handler: + for code_or_exception in f._blueprint_error_handler[member]: + blueprint.errorhandler(code_or_exception)(f) return blueprint def get_blueprint_kwargs(self): diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 14f277a8..33451e57 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -75,6 +75,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._refresh_configured_checks = False self._configured_checks = self._settings.get(["checks"], merged=True) update_check_hooks = self._plugin_manager.get_hooks("octoprint.plugin.softwareupdate.check_config") + check_providers = self._settings.get(["check_providers"], merged=True) for name, hook in update_check_hooks.items(): try: hook_checks = hook() @@ -82,9 +83,23 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._logger.exception("Error while retrieving update information from plugin {name}".format(**locals())) else: for key, data in hook_checks.items(): + check_providers[key] = name if key in self._configured_checks: data = dict_merge(data, self._configured_checks[key]) self._configured_checks[key] = data + self._settings.set(["check_providers"], check_providers) + self._settings.save() + + # we only want to process checks that came from plugins for + # which the plugins are still installed and enabled + config_checks = self._settings.get(["checks"]) + plugin_and_not_enabled = lambda k: k in check_providers and \ + not check_providers[k] in self._plugin_manager.enabled_plugins + obsolete_plugin_checks = filter(plugin_and_not_enabled, + config_checks.keys()) + for key in obsolete_plugin_checks: + self._logger.debug("Check for key {} was provided by plugin {} that's no longer available, ignoring it".format(key, check_providers[key])) + del self._configured_checks[key] return self._configured_checks @@ -150,6 +165,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, }, }, "pip_command": None, + "check_providers": {}, "cache_ttl": 24 * 60, } @@ -416,14 +432,13 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if not target in check_targets: continue - populated_check = self._populated_check(target, check) - try: + populated_check = self._populated_check(target, check) target_information, target_update_available, target_update_possible = self._get_current_version(target, populated_check, force=force) if target_information is None: target_information = dict() except exceptions.UnknownCheckType: - self._logger.warn("Unknown update check type for %s" % target) + self._logger.warn("Unknown update check type for target {}: {}".format(target, check.get("type", ""))) continue target_information = dict_merge(dict(local=dict(name="unknown", value="unknown"), remote=dict(name="unknown", value="unknown")), target_information) @@ -669,6 +684,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, raise exceptions.RestartFailed() def _populated_check(self, target, check): + if not "type" in check: + raise exceptions.UnknownCheckType() + result = dict(check) if target == "octoprint": @@ -703,30 +721,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, def _send_client_message(self, message_type, data=None): self._plugin_manager.send_plugin_message(self._identifier, dict(type=message_type, data=data)) - def _populated_check(self, target, check): - result = dict(check) - - if target == "octoprint": - from flask.ext.babel import gettext - result["displayName"] = check.get("displayName", gettext("OctoPrint")) - result["displayVersion"] = check.get("displayVersion", "{octoprint_version}") - - from octoprint._version import get_versions - versions = get_versions() - if check["type"] == "github_commit": - result["current"] = versions.get("full-revisionid", versions.get("full", "unknown")) - else: - result["current"] = versions["version"] - else: - result["displayName"] = check.get("displayName", target) - result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown")) - if check["type"] in ("github_commit"): - result["current"] = check.get("current", None) - else: - result["current"] = check.get("current", check.get("displayVersion", None)) - - return result - def _get_version_checker(self, target, check): """ Retrieves the version checker to use for given target and check configuration. Will raise an UnknownCheckType diff --git a/src/octoprint/plugins/virtual_printer/__init__.py b/src/octoprint/plugins/virtual_printer/__init__.py index ab66badd..e915aa00 100644 --- a/src/octoprint/plugins/virtual_printer/__init__.py +++ b/src/octoprint/plugins/virtual_printer/__init__.py @@ -7,25 +7,34 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms import octoprint.plugin + class VirtualPrinterPlugin(octoprint.plugin.SettingsPlugin): + def virtual_printer_factory(self, comm_instance, port, baudrate, + read_timeout): + if not port == "VIRTUAL": + return None - def virtual_printer_factory(self, comm_instance, port, baudrate, read_timeout): - if not port == "VIRTUAL": - return None + if not self._settings.global_get_boolean( + ["devel", "virtualPrinter", "enabled"]): + return None - if not self._settings.global_get_boolean(["devel", "virtualPrinter", "enabled"]): - return None + import logging + import logging.handlers - import logging - import logging.handlers + seriallog_handler = logging.handlers.RotatingFileHandler( + self._settings.get_plugin_logfile_path(postfix="serial"), + maxBytes=2 * 1024 * 1024) + seriallog_handler.setFormatter( + logging.Formatter("%(asctime)s %(message)s")) + seriallog_handler.setLevel(logging.DEBUG) - seriallog_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="serial"), maxBytes=2*1024*1024) - seriallog_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) - seriallog_handler.setLevel(logging.DEBUG) + from . import virtual + + serial_obj = virtual.VirtualPrinter( + seriallog_handler=seriallog_handler, + read_timeout=float(read_timeout)) + return serial_obj - from . import virtual - serial_obj = virtual.VirtualPrinter(seriallog_handler=seriallog_handler, read_timeout=float(read_timeout)) - return serial_obj __plugin_name__ = "Virtual Printer" __plugin_author__ = "Gina Häußge, based on work by Daid Braam" @@ -33,13 +42,14 @@ __plugin_homepage__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Virtual- __plugin_license__ = "AGPLv3" __plugin_description__ = "Provides a virtual printer via a virtual serial port for development and testing purposes" + def __plugin_load__(): - plugin = VirtualPrinterPlugin() + plugin = VirtualPrinterPlugin() - global __plugin_implementation__ - __plugin_implementation__ = plugin + global __plugin_implementation__ + __plugin_implementation__ = plugin - global __plugin_hooks__ - __plugin_hooks__ = { - "octoprint.comm.transport.serial.factory": plugin.virtual_printer_factory - } + global __plugin_hooks__ + __plugin_hooks__ = { + "octoprint.comm.transport.serial.factory": plugin.virtual_printer_factory + } diff --git a/src/octoprint/plugins/virtual_printer/virtual.py b/src/octoprint/plugins/virtual_printer/virtual.py index a6d5399e..e9becc5b 100644 --- a/src/octoprint/plugins/virtual_printer/virtual.py +++ b/src/octoprint/plugins/virtual_printer/virtual.py @@ -235,7 +235,7 @@ class VirtualPrinter(object): self._deleteSdFile(filename) elif "M114" in data: # send dummy position report - output = "C: X:10.00 Y:3.20 Z:5.20 E:1.24" + output = "C: X:{} Y:{} Z:{} E:{}".format(self._lastX, self._lastY, self._lastZ, self._lastE) if not self._okBeforeCommandOutput: output = "ok " + output self._send(output) @@ -243,6 +243,8 @@ class VirtualPrinter(object): elif "M117" in data: # we'll just use this to echo a message, to allow playing around with pause triggers self._send("echo:%s" % re.search("M117\s+(.*)", data).group(1)) + elif "M400" in data: + self.buffered.join() elif "M999" in data: # mirror Marlin behaviour self._send("Resend: 1") @@ -674,6 +676,7 @@ class VirtualPrinter(object): continue self._performMove(line) + self.buffered.task_done() def write(self, data): if self._debug_drop_connection: diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index a64b5109..d1be80d0 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -82,7 +82,7 @@ def on_identity_loaded(sender, identity): if user is None: return - identity.provides.add(UserNeed(user.get_name())) + identity.provides.add(UserNeed(user.get_id())) if user.is_user(): identity.provides.add(RoleNeed("user")) if user.is_admin(): @@ -99,9 +99,9 @@ def load_user(id): if userManager is not None: if sessionid: - return userManager.findUser(username=id, session=sessionid) + return userManager.findUser(userid=id, session=sessionid) else: - return userManager.findUser(username=id) + return userManager.findUser(userid=id) return users.DummyUser() diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index e14ab1ba..cf561a29 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -227,8 +227,10 @@ def passive_login(): user = flask.ext.login.current_user if user is not None and not user.is_anonymous(): - flask.g.user = user flask.ext.principal.identity_changed.send(flask.current_app._get_current_object(), identity=flask.ext.principal.Identity(user.get_id())) + if hasattr(user, "get_session"): + flask.session["usersession.id"] = user.get_session() + flask.g.user = user return flask.jsonify(user.asDict()) elif settings().getBoolean(["accessControl", "autologinLocal"]) \ and settings().get(["accessControl", "autologinAs"]) is not None \ @@ -252,7 +254,7 @@ def passive_login(): logger = logging.getLogger(__name__) logger.exception("Could not autologin user %s for networks %r" % (autologinAs, localNetworks)) - return ("", 204) + return "", 204 #~~ cache decorator for cacheable views @@ -281,11 +283,11 @@ def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=No if not callable(refreshif) or not refreshif(): rv = _cache.get(cache_key) if rv is not None: - logger.debug("Serving entry for {path} from cache".format(path=flask.request.path)) + logger.debug("Serving entry for {path} from cache (key: {key})".format(path=flask.request.path, key=cache_key)) return rv # get value from wrapped function - logger.debug("No cache entry or refreshing cache for {path}, calling wrapped function".format(path=flask.request.path)) + logger.debug("No cache entry or refreshing cache for {path} (key: {key}), calling wrapped function".format(path=flask.request.path, key=cache_key)) rv = f(*args, **kwargs) # do not store if the "unless_response" condition is true diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index be79b626..961ba445 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -22,21 +22,95 @@ from . import util import logging _logger = logging.getLogger(__name__) -@app.route("/") -@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, - key=lambda: "view/%s/%s" % (request.path, g.locale), - unless_response=util.flask.cache_check_response_headers) -def index(): +_templates = None +_plugin_names = None +_plugin_vars = None +@app.route("/") +def index(): + force_refresh = util.flask.cache_check_headers() or "_refresh" in request.values + + global _templates, _plugin_names, _plugin_vars + + if force_refresh or _templates is None or _plugin_names is None or _plugin_vars is None: + _templates, _plugin_names, _plugin_vars = _process_templates() + + now = datetime.datetime.utcnow() + render_kwargs = _get_render_kwargs(_templates, _plugin_names, _plugin_vars, now) + + def get_cached_view(key, view): + return util.flask.cached(refreshif=lambda: force_refresh, + key=lambda: "ui:{}:{}".format(key, g.locale), + unless_response=util.flask.cache_check_response_headers)(view) + + ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render") + for plugin in ui_plugins: + if plugin.will_handle_ui(request): + # plugin claims responsibility, let it render the UI + cached = get_cached_view(plugin._identifier, plugin.on_ui_render) + response = cached(now, request, render_kwargs) + if response is not None: + break + + else: + wizard = bool(_templates["wizard"]["order"]) + enable_accesscontrol = userManager is not None + + render_kwargs.update(dict( + webcamStream=settings().get(["webcam", "stream"]), + enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), + enableAccessControl=enable_accesscontrol, + enableSdSupport=settings().get(["feature", "sdSupport"]), + gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]), + gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]), + wizard=wizard, + now=now, + )) + + # no plugin took an interest, we'll use the default UI + def make_default_ui(): + r = make_response(render_template("index.jinja2", **render_kwargs)) + if bool(render_kwargs["templates"]["wizard"]["order"]): + r = util.flask.add_non_caching_response_headers(r) + return r + + cached = get_cached_view("_default", make_default_ui) + response = cached() + + response.headers["Last-Modified"] = now + + return response + + +def _get_render_kwargs(templates, plugin_names, plugin_vars, now): #~~ a bunch of settings + first_run = settings().getBoolean(["server", "firstRun"]) + locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES) + + #~~ prepare full set of template vars for rendering + + render_kwargs = dict( + debug=debug, + firstRun=first_run, + version=dict(number=VERSION, display=DISPLAY_VERSION, branch=BRANCH), + uiApiKey=UI_API_KEY, + templates=templates, + pluginNames=plugin_names, + locales=locales, + ) + render_kwargs.update(plugin_vars) + + return render_kwargs + + +def _process_templates(): + enable_accesscontrol = userManager is not None first_run = settings().getBoolean(["server", "firstRun"]) enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"])) enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0 - enable_accesscontrol = userManager is not None preferred_stylesheet = settings().get(["devel", "stylesheet"]) - locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES) ##~~ prepare templates @@ -327,43 +401,7 @@ def index(): templates[t]["entries"].update(template_sorting[t]["custom_insert_entries"](sorted_missing)) templates[t]["order"] = template_sorting[t]["custom_insert_order"](templates[t]["order"], sorted_missing) - #~~ prepare full set of template vars for rendering - - wizard = bool(templates["wizard"]["order"]) - now = datetime.datetime.utcnow() - render_kwargs = dict( - webcamStream=settings().get(["webcam", "stream"]), - enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), - enableAccessControl=enable_accesscontrol, - enableSdSupport=settings().get(["feature", "sdSupport"]), - firstRun=first_run, - debug=debug, - version=VERSION, - display_version=DISPLAY_VERSION, - branch=BRANCH, - gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]), - gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]), - uiApiKey=UI_API_KEY, - templates=templates, - pluginNames=plugin_names, - locales=locales, - wizard=wizard, - now=now - ) - render_kwargs.update(plugin_vars) - - #~~ render! - - response = make_response(render_template( - "index.jinja2", - **render_kwargs - )) - response.headers["Last-Modified"] = now - - if wizard: - response = util.flask.add_non_caching_response_headers(response) - - return response + return templates, plugin_names, plugin_vars def _process_template_configs(name, implementation, configs, rules): @@ -454,7 +492,8 @@ def robotsTxt(): @app.route("/i18n//.js") -@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale)) +@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, + key=lambda: "{}:{}".format(request.path, g.locale)) def localeJs(locale, domain): messages = dict() plural_expr = None diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index d3cc45c8..bde8b1cd 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -1072,7 +1072,7 @@ class Settings(object): def saveScript(self, script_type, name, script): script_folder = self.getBaseFolder("scripts") filename = os.path.realpath(os.path.join(script_folder, script_type, name)) - if not filename.startswith(script_folder): + if not filename.startswith(os.path.realpath(script_folder)): # oops, jail break, that shouldn't happen raise ValueError("Invalid script path to save to: {filename} (from {script_type}:{name})".format(**locals())) diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index 64a34ba1..7812fec5 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -585,7 +585,7 @@ class SlicingManager(object): name = self._sanitize(name) path = os.path.join(self.get_slicer_profile_path(slicer), "{name}.profile".format(name=name)) - if not os.path.realpath(path).startswith(self._profile_path): + if not os.path.realpath(path).startswith(os.path.realpath(self._profile_path)): raise IOError("Path to profile {name} tried to break out of allows sub path".format(**locals())) if must_exist and not (os.path.exists(path) and os.path.isfile(path)): raise UnknownProfile(slicer, name) diff --git a/src/octoprint/static/less/octoprint.less b/src/octoprint/static/less/octoprint.less index ab276cb5..28ec2a82 100644 --- a/src/octoprint/static/less/octoprint.less +++ b/src/octoprint/static/less/octoprint.less @@ -914,6 +914,7 @@ textarea.block { } .rotate90 { + -webkit-transform: rotate(-90deg); transform: rotate(-90deg); } diff --git a/src/octoprint/templates/initscript.jinja2 b/src/octoprint/templates/initscript.jinja2 index 28bf8eb5..e239de95 100644 --- a/src/octoprint/templates/initscript.jinja2 +++ b/src/octoprint/templates/initscript.jinja2 @@ -25,9 +25,9 @@ var SOCKJS_CLOSE_NORMAL = 1000; var UI_API_KEY = "{{ uiApiKey }}"; - var VERSION = "{{ version }}"; - var DISPLAY_VERSION = "{{ display_version }}"; - var BRANCH = "{{ branch }}"; + var VERSION = "{{ version.number }}"; + var DISPLAY_VERSION = "{{ version.display }}"; + var BRANCH = "{{ version.branch }}"; var LOCALE = "{{ g.locale }}"; var AVAILABLE_LOCALES = {{ locales|tojson }}; diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 6d844612..250f9bc9 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -23,23 +23,29 @@ class UserManager(object): def __init__(self): self._logger = logging.getLogger(__name__) self._session_users_by_session = dict() - self._session_users_by_username = dict() + self._session_users_by_userid = dict() def login_user(self, user): self._cleanup_sessions() - if user is None \ - or (isinstance(user, LocalProxy) and not isinstance(user._get_current_object(), User)) \ - or (not isinstance(user, LocalProxy) and not isinstance(user, User)): + if user is None: + return + + if isinstance(user, LocalProxy): + user = user._get_current_object() + + if not isinstance(user, User): return None if not isinstance(user, SessionUser): user = SessionUser(user) + self._session_users_by_session[user.get_session()] = user - if not user.get_name() in self._session_users_by_username: - self._session_users_by_username[user.get_name()] = [] - self._session_users_by_username[user.get_name()].append(user) + userid = user.get_id() + if not userid in self._session_users_by_userid: + self._session_users_by_userid[userid] = [] + self._session_users_by_userid[userid].append(user) self._logger.debug("Logged in user: %r" % user) @@ -49,14 +55,18 @@ class UserManager(object): if user is None: return + if isinstance(user, LocalProxy): + user = user._get_current_object() + if not isinstance(user, SessionUser): return - if user.get_name() in self._session_users_by_username: - users_by_username = self._session_users_by_username[user.get_name()] - for u in users_by_username: + userid = user.get_id() + if userid in self._session_users_by_userid: + users_by_userid = self._session_users_by_userid[userid] + for u in users_by_userid: if u.get_session() == user.get_session(): - users_by_username.remove(u) + users_by_userid.remove(u) break if user.get_session() in self._session_users_by_session: @@ -137,21 +147,19 @@ class UserManager(object): pass def removeUser(self, username): - if username in self._session_users_by_username: - users = self._session_users_by_username[username] + if username in self._session_users_by_userid: + users = self._session_users_by_userid[username] sessions = [user.get_session() for user in users if isinstance(user, SessionUser)] for session in sessions: if session in self._session_users_by_session: del self._session_users_by_session[session] - del self._session_users_by_username[username] + del self._session_users_by_userid[username] - def findUser(self, username=None, session=None): - if session is not None: - for session in self._session_users_by_session: - user = self._session_users_by_session[session] - if username is None or username == user.get_id(): - return user - break + def findUser(self, userid=None, session=None): + if session is not None and session in self._session_users_by_session: + user = self._session_users_by_session[session] + if userid is None or userid == user.get_id(): + return user return None @@ -345,16 +353,16 @@ class FilebasedUserManager(UserManager): self._dirty = True self._save() - def findUser(self, username=None, apikey=None, session=None): - user = UserManager.findUser(self, username=username, session=session) + def findUser(self, userid=None, apikey=None, session=None): + user = UserManager.findUser(self, userid=userid, session=session) if user is not None: return user - if username is not None: - if username not in self._users.keys(): + if userid is not None: + if userid not in self._users.keys(): return None - return self._users[username] + return self._users[userid] elif apikey is not None: for user in self._users.values(): @@ -413,7 +421,7 @@ class User(UserMixin): return self._passwordHash == passwordHash def get_id(self): - return self._username + return self.get_name() def get_name(self): return self._username diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index fd2042d2..5b7ca346 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -270,6 +270,7 @@ class MachineCom(object): sending=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sending"), sent=self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.sent") ) + self._received_message_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.gcode.received") self._printer_action_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.action") self._gcodescript_hooks = self._pluginManager.get_hooks("octoprint.comm.protocol.scripts") @@ -1416,9 +1417,6 @@ class MachineCom(object): self._errorValue = get_exception_string() self.close(is_error=True) return None - if ret == '': - #self._log("Recv: TIMEOUT") - return '' try: self._log("Recv: %s" % sanitize_ascii(ret)) @@ -1426,6 +1424,15 @@ class MachineCom(object): self._log("WARN: While reading last line: %s" % e) self._log("Recv: %r" % ret) + for name, hook in self._received_message_hooks.items(): + try: + ret = hook(self, ret) + except: + self._logger.exception("Error while processing hook {name}:".format(**locals())) + else: + if ret is None: + return "" + return ret def _getNext(self): diff --git a/src/octoprint/util/gcodeInterpreter.py b/src/octoprint/util/gcodeInterpreter.py index b0b69b66..7ac7508f 100644 --- a/src/octoprint/util/gcodeInterpreter.py +++ b/src/octoprint/util/gcodeInterpreter.py @@ -54,6 +54,9 @@ class gcode(object): absoluteE = True scale = 1.0 posAbs = True + fwretractTime = 0 + fwretractDist = 0 + fwrecoverTime = 0 feedRateXY = min(printer_profile["axes"]["x"]["speed"], printer_profile["axes"]["y"]["speed"]) if feedRateXY == 0: # some somewhat sane default if axes speeds are insane... @@ -172,6 +175,10 @@ class gcode(object): P = getCodeFloat(line, 'P') if P is not None: totalMoveTimeMinute += P / 60.0 / 1000.0 + elif G == 10: #Firmware retract + totalMoveTimeMinute += fwretractTime + elif G == 11: #Firmware retract recover + totalMoveTimeMinute += fwrecoverTime elif G == 20: #Units are inches scale = 25.4 elif G == 21: #Units are mm @@ -214,6 +221,15 @@ class gcode(object): absoluteE = True elif M == 83: #Relative E absoluteE = False + elif M == 207 or M == 208: #Firmware retract settings + s = getCodeFloat(line, 'S') + f = getCodeFloat(line, 'F') + if s is not None and f is not None: + if M == 207: + fwretractTime = s / f + fwretractDist = s + else: + fwrecoverTime = (fwretractDist + s) / f elif T is not None: if T > settings().getInt(["gcodeAnalysis", "maxExtruders"]):