Merge branch 'devel' into dev/folderSupport

This commit is contained in:
Salandora 2015-09-21 14:58:38 +02:00
commit 3e3a0b905f
24 changed files with 516 additions and 151 deletions

View file

@ -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

View file

@ -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)

View file

@ -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...

View file

@ -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 <https://github.com/OctoPrint/Plugin-Examples/blob/master/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

View file

@ -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:

2
run
View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python2
import os
import sys

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python2
# coding=utf-8
from setuptools import setup, find_packages

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python
#!/usr/bin/env python2
import sys
from octoprint.daemon import Daemon
from octoprint.server import Server

View file

@ -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:

View file

@ -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):

View file

@ -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 <sec-plugins-hook-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 <http://flask.pocoo.org/docs/0.10/api/#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 <https://github.com/OctoPrint/Plugin-Examples/blob/master/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 <https://github.com/OctoPrint/Plugin-Examples/blob/master/dummy_mobile_ui/templates/dummy_mobile_ui_index.jinja2>`_
Try installing the above plugin ``dummy_mobile_ui`` (also available in the
`plugin examples repository <https://github.com/OctoPrint/Plugin-Examples/blob/master/dummy_mobile_ui>`_)
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 <http://flask.pocoo.org/docs/0.10/api/#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 <http://flask.pocoo.org/docs/0.10/api/#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 <http://flask.pocoo.org/docs/0.10/api/#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 <http://flask.pocoo.org/docs/0.10/api/#flask.Blueprint.errorhandler>`_
and `the documentation for flask.Flask.errorhandler <http://flask.pocoo.org/docs/0.10/api/#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):

View file

@ -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", "<n/a>")))
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

View file

@ -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
}

View file

@ -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:

View file

@ -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()

View file

@ -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

View file

@ -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/<string:locale>/<string:domain>.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

View file

@ -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()))

View file

@ -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)

View file

@ -914,6 +914,7 @@ textarea.block {
}
.rotate90 {
-webkit-transform: rotate(-90deg);
transform: rotate(-90deg);
}

View file

@ -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 }};

View file

@ -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

View file

@ -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):

View file

@ -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"]):