Merge branch 'devel' into dev/gcodeScripts
Conflicts: CHANGELOG.md src/octoprint/static/css/octoprint.css src/octoprint/static/js/app/viewmodels/timelapse.js src/octoprint/util/comm.py
This commit is contained in:
commit
c3834c16fa
44 changed files with 1434 additions and 334 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -25,6 +25,8 @@
|
|||
- `action:pause`: Pauses the current job in OctoPrint
|
||||
- `action:resume`: Resumes the current job in OctoPrint
|
||||
- `action:disconnect`: Disconnects OctoPrint from the printer
|
||||
Plugins can add supported commands by [hooking](http://docs.octoprint.org/en/devel/plugins/hooks.html) into the
|
||||
``octoprint.comm.protocol.action`` hook
|
||||
* Mousing over the webcam image in the control tab enables key control mode, allowing you to quickly move the axis of your
|
||||
printer with your computer's keyboard ([#610](https://github.com/foosel/OctoPrint/pull/610)):
|
||||
- arrow keys: X and Y axes
|
||||
|
|
@ -83,6 +85,14 @@
|
|||
message for now.
|
||||
* Daemonized OctoPrint now cleans up its pidfile when receiving a TERM signal ([#711](https://github.com/foosel/OctoPrint/issues/711))
|
||||
* Added serial types for OpenBSD ([#551](https://github.com/foosel/OctoPrint/pull/551))
|
||||
* Improved behaviour of terminal:
|
||||
* Disabling autoscrolling now also stops cutting of the log while it's enabled, effectively preventing log lines from
|
||||
being modified at all ([#735](https://github.com/foosel/OctoPrint/issues/735))
|
||||
* Applying filters displays ``[...]`` where lines where removed
|
||||
* Added a link to scroll to the end of the terminal log (useful for when autoscroll is disabled)
|
||||
* Added a link to select all current contents of the terminal log for easy copy-pasting
|
||||
* Added a display of how many lines are displayed, how many are filtered and how many are available in total
|
||||
* Frame rate for timelapses can now be configured per timelapse ([#782](https://github.com/foosel/OctoPrint/pull/782))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
|
@ -126,6 +136,7 @@
|
|||
* Color code successful or failed print results directly in file list, not just after a reload
|
||||
* Changing Timelapse post roll activates save button
|
||||
* Timelapse post roll is loaded properly from config
|
||||
* Handling of files on the printer's SD card contained in folders now works correctly
|
||||
|
||||
([Commits](https://github.com/foosel/OctoPrint/compare/master...devel))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ recursive-include src/octoprint/plugins *
|
|||
recursive-include src/octoprint/translations *
|
||||
include versioneer.py
|
||||
include src/octoprint/_version.py
|
||||
include README.md
|
||||
include requirements.txt
|
||||
|
|
|
|||
18
docs/conf.py
18
docs/conf.py
|
|
@ -27,11 +27,11 @@ year_current = date.today().year
|
|||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
#needs_sphinx = '1.3'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain']
|
||||
extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinxcontrib.httpdomain', 'sphinx.ext.autosummary', 'sphinxcontrib.napoleon']
|
||||
todo_include_todos = True
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
|
|
@ -103,9 +103,9 @@ pygments_style = 'sphinx'
|
|||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
|
|
@ -139,11 +139,9 @@ if not on_rtd: # only import and set the theme if we're building docs locally
|
|||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_context = {
|
||||
'css_files': [
|
||||
'_static/theme_overrides.css', # overrides for wide tables in RTD theme
|
||||
],
|
||||
}
|
||||
|
||||
def setup(app):
|
||||
app.add_stylesheet("theme_overrides.css")
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.. _sec-development-interface:
|
||||
|
||||
Interfaces
|
||||
==========
|
||||
Internal API
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
|
@ -9,3 +9,4 @@ Interfaces
|
|||
filemanager.rst
|
||||
plugin.rst
|
||||
printer.rst
|
||||
util.rst
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
.. _sec-development-interface-plugin:
|
||||
|
||||
``octoprint.plugin``
|
||||
--------------------
|
||||
octoprint.plugin
|
||||
----------------
|
||||
|
||||
.. automodule:: octoprint.plugin
|
||||
:members: plugin_manager, plugin_settings, call_plugin, PluginSettings
|
||||
:undoc-members:
|
||||
|
||||
.. _sec-development-interface-plugin-core:
|
||||
|
||||
octoprint.plugin.core
|
||||
---------------------
|
||||
|
||||
.. automodule:: octoprint.plugin.core
|
||||
|
||||
octoprint.plugin.types
|
||||
----------------------
|
||||
|
||||
.. automodule:: octoprint.plugin.types
|
||||
|
||||
|
|
|
|||
9
docs/development/interface/util.rst
Normal file
9
docs/development/interface/util.rst
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.. _sec-development-interface-util:
|
||||
|
||||
octoprint.util
|
||||
--------------
|
||||
|
||||
.. automodule:: octoprint.util
|
||||
:members:
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,99 @@
|
|||
Available plugin mixins
|
||||
=======================
|
||||
|
||||
.. automodule:: octoprint.plugin
|
||||
:members: AppPlugin, AssetPlugin, BlueprintPlugin, EventHandlerPlugin, ProgressPlugin, SettingsPlugin, ShutdownPlugin, SimpleApiPlugin, SlicerPlugin, StartupPlugin, TemplatePlugin
|
||||
:undoc-members:
|
||||
The following plugin mixins are currently available:
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Please note that all plugin mixins inherit from :class:`~octoprint.plugin.core.Plugin` and
|
||||
:class:`~octoprint.plugin.OctoPrintPlugin`, which also provide attributes of interest to plugin developers.
|
||||
|
||||
.. _sec-plugins-mixins-startupplugin:
|
||||
|
||||
StartupPlugin
|
||||
-------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.StartupPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-shutdownplugin:
|
||||
|
||||
ShutdownPlugin
|
||||
--------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.ShutdownPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-settingsplugin:
|
||||
|
||||
SettingsPlugin
|
||||
--------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.SettingsPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-assetplugin:
|
||||
|
||||
AssetPlugin
|
||||
-----------
|
||||
|
||||
.. autoclass:: octoprint.plugin.AssetPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-templateplugin:
|
||||
|
||||
TemplatePlugin
|
||||
--------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.TemplatePlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-simpleapiplugin:
|
||||
|
||||
SimpleApiPlugin
|
||||
---------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.SimpleApiPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-blueprintplugin:
|
||||
|
||||
BlueprintPlugin
|
||||
---------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.BlueprintPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-eventhandlerplugin:
|
||||
|
||||
EventHandlerPlugin
|
||||
------------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.EventHandlerPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-progressplugin:
|
||||
|
||||
ProgressPlugin
|
||||
--------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.ProgressPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-slicerplugin:
|
||||
|
||||
SlicerPlugin
|
||||
------------
|
||||
|
||||
.. autoclass:: octoprint.plugin.SlicerPlugin
|
||||
:members:
|
||||
|
||||
.. _sec-plugins-mixins-appplugin:
|
||||
|
||||
AppPlugin
|
||||
---------
|
||||
|
||||
.. autoclass:: octoprint.plugin.AppPlugin
|
||||
:members:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-napoleon
|
||||
sphinx_rtd_theme
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import tempfile
|
|||
|
||||
import octoprint.filemanager
|
||||
|
||||
from octoprint.util import safeRename
|
||||
from octoprint.util import safe_rename
|
||||
|
||||
class StorageInterface(object):
|
||||
"""
|
||||
|
|
@ -1003,7 +1003,7 @@ class LocalFileStorage(StorageInterface):
|
|||
with open(metadata_temporary_path, "w") as f:
|
||||
import yaml
|
||||
yaml.safe_dump(metadata, stream=f, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
safeRename(metadata_temporary_path, metadata_path, throw_error=True)
|
||||
safe_rename(metadata_temporary_path, metadata_path, throw_error=True)
|
||||
except:
|
||||
self._logger.exception("Error while writing .metadata.yaml to {path}".format(**locals()))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,19 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
This module represents OctoPrint's plugin subsystem. This includes management and helper methods as well as the
|
||||
registered plugin types.
|
||||
|
||||
.. autofunction:: plugin_manager
|
||||
|
||||
.. autofunction:: plugin_settings
|
||||
|
||||
.. autofunction:: call_plugin
|
||||
|
||||
.. autoclass:: PluginSettings
|
||||
:members:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
|
@ -12,13 +27,47 @@ from octoprint.settings import settings
|
|||
from octoprint.plugin.core import (PluginInfo, PluginManager, Plugin)
|
||||
from octoprint.plugin.types import *
|
||||
|
||||
from octoprint.util import deprecated
|
||||
|
||||
# singleton
|
||||
_instance = None
|
||||
|
||||
def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None):
|
||||
"""
|
||||
Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager`
|
||||
singleton.
|
||||
|
||||
Arguments:
|
||||
init (boolean): A flag indicating whether this is the initial call to construct the singleton (True) or not
|
||||
(False, default). If this is set to True and the plugin manager has already been initialized, a :class:`ValueError`
|
||||
will be raised. The same will happen if the plugin manager has not yet been initialized and this is set to
|
||||
False.
|
||||
plugin_folders (list): A list of folders (as strings containing the absolute path to them) in which to look for
|
||||
potential plugin modules. If not provided this defaults to the configured ``plugins`` base folder and
|
||||
``src/plugins`` within OctoPrint's code base.
|
||||
plugin_types (list): A list of recognized plugin types for which to look for provided implementations. If not
|
||||
provided this defaults to the plugin types found in :mod:`octoprint.plugin.types` without
|
||||
:class:`~octoprint.plugin.OctoPrintPlugin`.
|
||||
plugin_entry_points (list): A list of entry points pointing to modules which to load as plugins. If not provided
|
||||
this defaults to the entry point ``octoprint.plugin``.
|
||||
plugin_disabled_list (list): A list of plugin identifiers that are currently disabled. If not provided this
|
||||
defaults to all plugins for which ``enabled`` is set to ``False`` in the settings.
|
||||
|
||||
Returns:
|
||||
PluginManager: A fully initialized :class:`~octoprint.plugin.core.PluginManager` instance to be used for plugin
|
||||
management tasks.
|
||||
|
||||
Raises:
|
||||
ValueError: ``init`` was True although the plugin manager was already initialized, or it was False although
|
||||
the plugin manager was not yet initialized.
|
||||
"""
|
||||
|
||||
global _instance
|
||||
if _instance is None:
|
||||
if init:
|
||||
if _instance is not None:
|
||||
raise ValueError("Plugin Manager already initialized")
|
||||
|
||||
if plugin_folders is None:
|
||||
plugin_folders = (settings().getBaseFolder("plugins"), os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")))
|
||||
if plugin_types is None:
|
||||
|
|
@ -49,10 +98,59 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
|
|||
|
||||
|
||||
def plugin_settings(plugin_key, defaults=None):
|
||||
"""
|
||||
Factory method for creating a :class:`PluginSettings` instance.
|
||||
|
||||
Arguments:
|
||||
plugin_key (string): The plugin identifier for which to create the settings instance.
|
||||
defaults (dict): The default settings for the plugin.
|
||||
|
||||
Returns:
|
||||
PluginSettings: A fully initialized :class:`PluginSettings` instance to be used to access the plugin's
|
||||
settings
|
||||
"""
|
||||
return PluginSettings(settings(), plugin_key, defaults=defaults)
|
||||
|
||||
|
||||
def call_plugin(types, method, args=None, kwargs=None, callback=None, error_callback=None):
|
||||
"""
|
||||
Helper method to invoke the indicated ``method`` on all registered plugin implementations implementing the
|
||||
indicated ``types``. Allows providing method arguments and registering callbacks to call in case of success
|
||||
and/or failure of each call which can be used to return individual results to the calling code.
|
||||
|
||||
Example:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def my_success_callback(name, plugin, result):
|
||||
print("{name} was called successfully and returned {result!r}".format(**locals()))
|
||||
|
||||
def my_error_callback(name, plugin, exc):
|
||||
print("{name} raised an exception: {exc!s}".format(**locals()))
|
||||
|
||||
octoprint.plugin.call_plugin(
|
||||
[octoprint.plugin.StartupPlugin],
|
||||
"on_startup",
|
||||
args=(my_host, my_port),
|
||||
callback=my_success_callback,
|
||||
error_callback=my_error_callback
|
||||
)
|
||||
|
||||
Arguments:
|
||||
types (list): A list of plugin implementation types to match against.
|
||||
method (string): Name of the method to call on all matching implementations.
|
||||
args (tuple): A tuple containing the arguments to supply to the called ``method``. Optional.
|
||||
kwargs (dict): A dictionary containing the keyword arguments to supply to the called ``method``. Optional.
|
||||
callback (function): A callback to invoke after an implementation has been called successfully. Will be called
|
||||
with the three arguments ``name``, ``plugin`` and ``result``. ``name`` will be the plugin identifier,
|
||||
``plugin`` the plugin implementation instance itself and ``result`` the result returned from the
|
||||
``method`` invocation.
|
||||
error_callback (function): A callback to invoke after the call of an implementation resulted in an exception.
|
||||
Will be called with the three arguments ``name``, ``plugin`` and ``exc``. ``name`` will be the plugin
|
||||
identifier, ``plugin`` the plugin implementation instance itself and ``exc`` the caught exception.
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
if args is None:
|
||||
|
|
@ -67,14 +165,53 @@ def call_plugin(types, method, args=None, kwargs=None, callback=None, error_call
|
|||
result = getattr(plugin, method)(*args, **kwargs)
|
||||
if callback:
|
||||
callback(name, plugin, result)
|
||||
except Exception as e:
|
||||
except Exception as exc:
|
||||
logging.getLogger(__name__).exception("Error while calling plugin %s" % name)
|
||||
if error_callback:
|
||||
error_callback(name, plugin, e)
|
||||
error_callback(name, plugin, exc)
|
||||
|
||||
|
||||
class PluginSettings(object):
|
||||
"""
|
||||
The :class:`PluginSettings` class is the interface for plugins to their own or globally defined settings.
|
||||
|
||||
It provides a couple of convenience methods for directly accessing plugin settings via the regular
|
||||
:class:`octoprint.settings.Settings` interfaces as well as means to access plugin specific folder locations.
|
||||
|
||||
.. method:: get(path, merged=False, asdict=False)
|
||||
|
||||
Retrieves a raw key from the settings for ``path``, optionally merging the raw value with the default settings
|
||||
if ``merged`` is set to True.
|
||||
|
||||
:param list path: a list of path elements to navigate to the settings value
|
||||
:param boolean merged: whether to merge the returned result with the default settings (True) or not (False, default)
|
||||
:return: the retrieved settings value
|
||||
|
||||
.. method:: get_int(path)
|
||||
|
||||
.. method:: get_float(path)
|
||||
|
||||
.. method:: get_boolean(path)
|
||||
|
||||
.. method:: set(path, value, force=False)
|
||||
|
||||
.. method:: set_int(path, value, force=False)
|
||||
|
||||
.. method:: set_float(path, value, force=False)
|
||||
|
||||
.. method:: set_boolean(path, value, force=False)
|
||||
"""
|
||||
|
||||
def __init__(self, settings, plugin_key, defaults=None):
|
||||
"""
|
||||
Initializes the object with the provided :class:`octoprint.settings.Settings` manager as ``settings``, using
|
||||
the ``plugin_key`` and optional ``defaults``.
|
||||
|
||||
:param settings:
|
||||
:param plugin_key:
|
||||
:param defaults:
|
||||
:return:
|
||||
"""
|
||||
self.settings = settings
|
||||
self.plugin_key = plugin_key
|
||||
|
||||
|
|
@ -103,45 +240,46 @@ class PluginSettings(object):
|
|||
kwargs.update(dict(defaults=self.defaults))
|
||||
return kwargs
|
||||
|
||||
self.access_methods = {
|
||||
'get': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'getInt': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'getFloat': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'getBoolean': (lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'set': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'setInt': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'setFloat': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
'setBoolean': (lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs))
|
||||
}
|
||||
self.access_methods = dict(
|
||||
get=("get", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
get_int=("getInt", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
get_float=("getFloat", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
get_boolean=("getBoolean", lambda args,: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
set=("set", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
set_int=("setInt", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
set_float=("setFloat", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs)),
|
||||
set_boolean=("setBoolean", lambda args: prefix_path_in_args(args), lambda kwargs: add_defaults_to_kwargs(kwargs))
|
||||
)
|
||||
self.deprecated_access_methods = dict(getInt="get_int", getFloat="get_float", getBoolean="get_boolean", setInt="set_int", setFloat="set_float", setBoolean="set_boolean")
|
||||
|
||||
def globalGet(self, path):
|
||||
return self.settings.get(path)
|
||||
def global_get(self, path, **kwargs):
|
||||
return self.settings.get(path, **kwargs)
|
||||
|
||||
def globalGetInt(self, path):
|
||||
return self.settings.getInt(path)
|
||||
def global_get_int(self, path, **kwargs):
|
||||
return self.settings.getInt(path, **kwargs)
|
||||
|
||||
def globalGetFloat(self, path):
|
||||
return self.settings.getFloat(path)
|
||||
def global_get_float(self, path, **kwargs):
|
||||
return self.settings.getFloat(path, **kwargs)
|
||||
|
||||
def globalGetBoolean(self, path):
|
||||
return self.settings.getBoolean(path)
|
||||
def global_get_boolean(self, path, **kwargs):
|
||||
return self.settings.getBoolean(path, **kwargs)
|
||||
|
||||
def globalSet(self, path, value):
|
||||
self.settings.set(path, value)
|
||||
def global_set(self, path, value, **kwargs):
|
||||
self.settings.set(path, value, **kwargs)
|
||||
|
||||
def globalSetInt(self, path, value):
|
||||
self.settings.setInt(path, value)
|
||||
def global_set_int(self, path, value, **kwargs):
|
||||
self.settings.setInt(path, value, **kwargs)
|
||||
|
||||
def globalSetFloat(self, path, value):
|
||||
self.settings.setFloat(path, value)
|
||||
def global_set_float(self, path, value, **kwargs):
|
||||
self.settings.setFloat(path, value, **kwargs)
|
||||
|
||||
def globalSetBoolean(self, path, value):
|
||||
self.settings.setBoolean(path, value)
|
||||
def global_set_boolean(self, path, value, **kwargs):
|
||||
self.settings.setBoolean(path, value, **kwargs)
|
||||
|
||||
def globalGetBaseFolder(self, folder_type):
|
||||
return self.settings.getBaseFolder(folder_type)
|
||||
def global_get_basefolder(self, folder_type, **kwargs):
|
||||
return self.settings.getBaseFolder(folder_type, **kwargs)
|
||||
|
||||
def getPluginLogfilePath(self, postfix=None):
|
||||
def get_plugin_logfile_path(self, postfix=None):
|
||||
filename = "plugin_" + self.plugin_key
|
||||
if postfix is not None:
|
||||
filename += "_" + postfix
|
||||
|
|
@ -149,10 +287,60 @@ class PluginSettings(object):
|
|||
return os.path.join(self.settings.getBaseFolder("logs"), filename)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.access_methods and hasattr(self.settings, item) and callable(getattr(self.settings, item)):
|
||||
orig_item = getattr(self.settings, item)
|
||||
args_mapper, kwargs_mapper = self.access_methods[item]
|
||||
all_access_methods = self.access_methods.keys() + self.deprecated_access_methods.keys()
|
||||
if item in all_access_methods:
|
||||
decorator = None
|
||||
if item in self.deprecated_access_methods:
|
||||
new = self.deprecated_access_methods[item]
|
||||
decorator = deprecated("{old} has been renamed to {new}".format(old=item, new=new), stacklevel=2)
|
||||
item = new
|
||||
|
||||
return lambda *args, **kwargs: orig_item(*args_mapper(args), **kwargs_mapper(kwargs))
|
||||
else:
|
||||
return getattr(self.settings, item)
|
||||
settings_name, args_mapper, kwargs_mapper = self.access_methods[item]
|
||||
if hasattr(self.settings, settings_name) and callable(getattr(self.settings, settings_name)):
|
||||
orig_func = getattr(self.settings, settings_name)
|
||||
if decorator is not None:
|
||||
orig_func = decorator(orig_func)
|
||||
|
||||
def _func(*args, **kwargs):
|
||||
return orig_func(*args_mapper(args), **kwargs_mapper(kwargs))
|
||||
_func.__name__ = item
|
||||
_func.__doc__ = orig_func.__doc__ if "__doc__" in dir(orig_func) else None
|
||||
|
||||
return _func
|
||||
|
||||
return getattr(self.settings, item)
|
||||
|
||||
##~~ deprecated methods follow
|
||||
|
||||
# TODO: Remove with release of 1.2.0
|
||||
|
||||
globalGet = deprecated("globalGet has been renamed to global_get",
|
||||
includedoc="Replaced by :func:`global_get`",
|
||||
since="1.2.0-dev-546")(global_get)
|
||||
globalGetInt = deprecated("globalGetInt has been renamed to global_get_int",
|
||||
includedoc="Replaced by :func:`global_get_int`",
|
||||
since="1.2.0-dev-546")(global_get_int)
|
||||
globalGetFloat = deprecated("globalGetFloat has been renamed to global_get_float",
|
||||
includedoc="Replaced by :func:`global_get_float`",
|
||||
since="1.2.0-dev-546")(global_get_float)
|
||||
globalGetBoolean = deprecated("globalGetBoolean has been renamed to global_get_boolean",
|
||||
includedoc="Replaced by :func:`global_get_boolean`",
|
||||
since="1.2.0-dev-546")(global_get_boolean)
|
||||
globalSet = deprecated("globalSet has been renamed to global_set",
|
||||
includedoc="Replaced by :func:`global_set`",
|
||||
since="1.2.0-dev-546")(global_set)
|
||||
globalSetInt = deprecated("globalSetInt has been renamed to global_set_int",
|
||||
includedoc="Replaced by :func:`global_set_int`",
|
||||
since="1.2.0-dev-546")(global_set_int)
|
||||
globalSetFloat = deprecated("globalSetFloat has been renamed to global_set_float",
|
||||
includedoc="Replaced by :func:`global_set_float`",
|
||||
since="1.2.0-dev-546")(global_set_float)
|
||||
globalSetBoolean = deprecated("globalSetBoolean has been renamed to global_set_boolean",
|
||||
includedoc="Replaced by :func:`global_set_boolean`",
|
||||
since="1.2.0-dev-546")(global_set_boolean)
|
||||
globalGetBaseFolder = deprecated("globalGetBaseFolder has been renamed to global_get_basefolder",
|
||||
includedoc="Replaced by :func:`global_get_basefolder`",
|
||||
since="1.2.0-dev-546")(global_get_basefolder)
|
||||
getPluginLogfilePath = deprecated("getPluginLogfilePath has been renamed to get_plugin_logfile_path",
|
||||
includedoc="Replaced by :func:`get_plugin_logfile_path`",
|
||||
since="1.2.0-dev-546")(get_plugin_logfile_path)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
In this module resides the core data structures and logic of the plugin system. It is implemented in an OctoPrint-agnostic
|
||||
way and could be extracted into a separate Python module in the future.
|
||||
|
||||
.. autoclass:: PluginManager
|
||||
|
||||
.. autoclass:: PluginInfo
|
||||
|
||||
.. autoclass:: Plugin
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
|
@ -13,6 +25,15 @@ import logging
|
|||
|
||||
|
||||
class PluginInfo(object):
|
||||
"""
|
||||
The :class:`PluginInfo` class wraps all available information about a registered plugin.
|
||||
|
||||
This includes its meta data (like name, description, version, etc) as well as the actual plugin extensions like
|
||||
implementations, hooks and helpers.
|
||||
|
||||
It works on Python module objects and extracts the relevant data from those via accessing the
|
||||
:ref:`control properties <sec-plugins-infrastructure-controlproperties>`.
|
||||
"""
|
||||
|
||||
attr_name = '__plugin_name__'
|
||||
|
||||
|
|
@ -125,6 +146,12 @@ class PluginInfo(object):
|
|||
|
||||
|
||||
class PluginManager(object):
|
||||
"""
|
||||
The :class:`PluginManager` is the central component for finding, loading and accessing plugins provided to the
|
||||
system.
|
||||
|
||||
It is able to discover plugins both through possible file system locations as well as customizable entry points.
|
||||
"""
|
||||
|
||||
def __init__(self, plugin_folders, plugin_types, plugin_entry_points, logging_prefix=None, plugin_disabled_list=None):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
|
@ -405,5 +432,34 @@ class PluginManager(object):
|
|||
|
||||
|
||||
class Plugin(object):
|
||||
"""
|
||||
The parent class of all plugin implementations.
|
||||
|
||||
.. attribute:: _identifier
|
||||
|
||||
The identifier of the plugin. Injected by the plugin core system upon initialization of the implementation.
|
||||
|
||||
.. attribute:: _plugin_name
|
||||
|
||||
The name of the plugin. Injected by the plugin core system upon initialization of the implementation.
|
||||
|
||||
.. attribute:: _plugin_version
|
||||
|
||||
The version of the plugin. Injected by the plugin core system upon initialization of the implementation.
|
||||
|
||||
.. attribute:: _basefolder
|
||||
|
||||
The base folder of the plugin. Injected by the plugin core system upon initialization of the implementation.
|
||||
|
||||
.. attribute:: _logger
|
||||
|
||||
The logger instance to use, with the logging name set to the :attr:`PluginManager.logging_prefix` of the
|
||||
:class:`PluginManager` concatenated with :attr:`_identifier`. Injected by the plugin core system upon
|
||||
initialization of the implementation.
|
||||
"""
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Called by the plugin core after performing all injections.
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
This module bundles all of OctoPrint's supported plugin implementation types as well as their common parent
|
||||
class, :class:`OctoPrintPlugin`.
|
||||
|
||||
Please note that the plugin implementation types are documented in the section
|
||||
:ref:`Available plugin mixins <sec-plugins-mixins>`.
|
||||
|
||||
.. autoclass: OctoPrintPlugin
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
|
|
@ -9,7 +20,29 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
from .core import Plugin
|
||||
|
||||
|
||||
class StartupPlugin(Plugin):
|
||||
class OctoPrintPlugin(Plugin):
|
||||
"""
|
||||
.. attribute:: _plugin_manager
|
||||
|
||||
.. attribute:: _printer_profile_manager
|
||||
|
||||
.. attribute:: _event_bus
|
||||
|
||||
.. attribute:: _analysis_queue
|
||||
|
||||
.. attribute:: _slicing_manager
|
||||
|
||||
.. attribute:: _file_manager
|
||||
|
||||
.. attribute:: _printer
|
||||
|
||||
.. attribute:: _app_session_manager
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StartupPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
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.
|
||||
|
|
@ -37,7 +70,7 @@ class StartupPlugin(Plugin):
|
|||
pass
|
||||
|
||||
|
||||
class ShutdownPlugin(Plugin):
|
||||
class ShutdownPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
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`
|
||||
|
|
@ -51,14 +84,14 @@ class ShutdownPlugin(Plugin):
|
|||
pass
|
||||
|
||||
|
||||
class AssetPlugin(Plugin):
|
||||
class AssetPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
The ``AssetPlugin`` mixin allows plugins to define additional static assets such as Javascript or CSS files to
|
||||
be automatically embedded into the pages delivered by the server to be used within the client sided part of
|
||||
the plugin.
|
||||
|
||||
A typical usage of the ``AssetPlugin`` functionality is to embed a custom view model to be used by templates injected
|
||||
through :class:`TemplatePlugin`s.
|
||||
through a :class:`TemplatePlugin`.
|
||||
"""
|
||||
|
||||
def get_asset_folder(self):
|
||||
|
|
@ -104,7 +137,7 @@ class AssetPlugin(Plugin):
|
|||
return dict()
|
||||
|
||||
|
||||
class TemplatePlugin(Plugin):
|
||||
class TemplatePlugin(OctoPrintPlugin):
|
||||
"""
|
||||
Using the ``TemplatePlugin`` mixin plugins may inject their own components into the OctoPrint web interface.
|
||||
|
||||
|
|
@ -408,7 +441,7 @@ class TemplatePlugin(Plugin):
|
|||
return os.path.join(self._basefolder, "templates")
|
||||
|
||||
|
||||
class SimpleApiPlugin(Plugin):
|
||||
class SimpleApiPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
Utilizing the ``SimpleApiPlugin`` mixin plugins may implement a simple API based around one GET resource and one
|
||||
resource accepting JSON commands POSTed to it. This is the easy alternative for plugin's which don't need the
|
||||
|
|
@ -543,7 +576,7 @@ class SimpleApiPlugin(Plugin):
|
|||
return None
|
||||
|
||||
|
||||
class BlueprintPlugin(Plugin):
|
||||
class BlueprintPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
The ``BlueprintPlugin`` mixin allows plugins to define their own full fledged endpoints for whatever purpose,
|
||||
be it a more sophisticated API than what is possible via the :class:`SimpleApiPlugin` or a custom web frontend.
|
||||
|
|
@ -646,7 +679,7 @@ class BlueprintPlugin(Plugin):
|
|||
return True
|
||||
|
||||
|
||||
class SettingsPlugin(Plugin):
|
||||
class SettingsPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
Including the ``SettingsPlugin`` mixin allows plugins to store and retrieve their own settings within OctoPrint's
|
||||
configuration.
|
||||
|
|
@ -697,6 +730,11 @@ class SettingsPlugin(Plugin):
|
|||
|
||||
Of course, you are always free to completely override both :func:`on_settings_load` and :func:`on_settings_save` if the
|
||||
default implementations do not fit your requirements.
|
||||
|
||||
.. attribute:: _settings
|
||||
|
||||
The :class:`~octoprint.plugin.PluginSettings` instance to use for accessing the plugin's settings. Injected by
|
||||
the plugin core system upon initialization of the implementation.
|
||||
"""
|
||||
|
||||
def on_settings_load(self):
|
||||
|
|
@ -751,7 +789,7 @@ class SettingsPlugin(Plugin):
|
|||
return dict()
|
||||
|
||||
|
||||
class EventHandlerPlugin(Plugin):
|
||||
class EventHandlerPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
The ``EventHandlerPlugin`` mixin allows OctoPrint plugins to react to any of :ref:`OctoPrint's events <sec-events>`.
|
||||
OctoPrint will call the :func:`on_event` method for any event fired on its internal event bus, supplying the
|
||||
|
|
@ -774,7 +812,7 @@ class EventHandlerPlugin(Plugin):
|
|||
pass
|
||||
|
||||
|
||||
class SlicerPlugin(Plugin):
|
||||
class SlicerPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
Via the ``SlicerPlugin`` mixin plugins can add support for slicing engines to be used by OctoPrint.
|
||||
|
||||
|
|
@ -835,10 +873,10 @@ class SlicerPlugin(Plugin):
|
|||
``False`` (defaults to ``True``), an ``IOError`` should be raised.
|
||||
|
||||
:param string path: the path to which to save the profile
|
||||
:param :class:`SlicingProfile` profile: the profile to save
|
||||
:param SlicingProfile profile: the profile to save
|
||||
:param bool allow_overwrite: whether to allow to overwrite an existing profile at the indicated path (``True``, default)
|
||||
or not (``False``) - if a profile already exists on the path and this is ``False``
|
||||
and :class:`IOError` should be raised
|
||||
or not (``False``) - if a profile already exists on the path and this is ``False``
|
||||
and :class:`IOError` should be raised
|
||||
:param dict overrides: profile overrides to apply to the ``profile`` before saving it
|
||||
"""
|
||||
pass
|
||||
|
|
@ -881,7 +919,7 @@ class SlicerPlugin(Plugin):
|
|||
pass
|
||||
|
||||
|
||||
class ProgressPlugin(Plugin):
|
||||
class ProgressPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
Via the ``ProgressPlugin`` mixing plugins can let themselves be called upon progress in print jobs or slicing jobs,
|
||||
limited to minimally 1% steps.
|
||||
|
|
@ -911,7 +949,7 @@ class ProgressPlugin(Plugin):
|
|||
pass
|
||||
|
||||
|
||||
class AppPlugin(Plugin):
|
||||
class AppPlugin(OctoPrintPlugin):
|
||||
def get_additional_apps(self):
|
||||
return []
|
||||
|
||||
|
|
|
|||
|
|
@ -39,26 +39,26 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
|
||||
def on_startup(self, host, port):
|
||||
# setup our custom logger
|
||||
cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.getPluginLogfilePath(postfix="engine"), maxBytes=2*1024*1024)
|
||||
cura_logging_handler = logging.handlers.RotatingFileHandler(self._settings.get_plugin_logfile_path(postfix="engine"), maxBytes=2*1024*1024)
|
||||
cura_logging_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||
cura_logging_handler.setLevel(logging.DEBUG)
|
||||
|
||||
self._cura_logger.addHandler(cura_logging_handler)
|
||||
self._cura_logger.setLevel(logging.DEBUG if self._settings.getBoolean(["debug_logging"]) else logging.CRITICAL)
|
||||
self._cura_logger.setLevel(logging.DEBUG if self._settings.get_boolean(["debug_logging"]) else logging.CRITICAL)
|
||||
self._cura_logger.propagate = False
|
||||
|
||||
##~~ BlueprintPlugin API
|
||||
|
||||
@octoprint.plugin.BlueprintPlugin.route("/import", methods=["POST"])
|
||||
def importCuraProfile(self):
|
||||
def import_cura_profile(self):
|
||||
import datetime
|
||||
import tempfile
|
||||
|
||||
from octoprint.server import slicingManager
|
||||
|
||||
input_name = "file"
|
||||
input_upload_name = input_name + "." + self._settings.globalGet(["server", "uploads", "nameSuffix"])
|
||||
input_upload_path = input_name + "." + self._settings.globalGet(["server", "uploads", "pathSuffix"])
|
||||
input_upload_name = input_name + "." + self._settings.global_get(["server", "uploads", "nameSuffix"])
|
||||
input_upload_path = input_name + "." + self._settings.global_get(["server", "uploads", "pathSuffix"])
|
||||
|
||||
if input_upload_name in flask.request.values and input_upload_path in flask.request.values:
|
||||
filename = flask.request.values[input_upload_name]
|
||||
|
|
@ -92,7 +92,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
# default values for name, display name and description
|
||||
profile_name = _sanitize_name(name)
|
||||
profile_display_name = name
|
||||
profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.getFormattedDateTime(datetime.datetime.now()))
|
||||
profile_description = "Imported from {filename} on {date}".format(filename=filename, date=octoprint.util.get_formatted_datetime(datetime.datetime.now()))
|
||||
profile_allow_overwrite = False
|
||||
|
||||
# overrides
|
||||
|
|
@ -134,11 +134,11 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
##~~ SettingsPlugin API
|
||||
|
||||
def on_settings_save(self, data):
|
||||
old_debug_logging = self._settings.getBoolean(["debug_logging"])
|
||||
old_debug_logging = self._settings.get_boolean(["debug_logging"])
|
||||
|
||||
super(CuraPlugin, self).on_settings_save(data)
|
||||
|
||||
new_debug_logging = self._settings.getBoolean(["debug_logging"])
|
||||
new_debug_logging = self._settings.get_boolean(["debug_logging"])
|
||||
if old_debug_logging != new_debug_logging:
|
||||
if new_debug_logging:
|
||||
self._cura_logger.setLevel(logging.DEBUG)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
<div class="input-prepend">
|
||||
<span class="btn fileinput-button">
|
||||
<span>{{ _('Browse...') }}</span>
|
||||
<input id="settings-cura-import" type="file" name="file" data-url="{{ url_for("plugin.cura.importCuraProfile") }}">
|
||||
<input id="settings-cura-import" type="file" name="file" data-url="{{ url_for("plugin.cura.import_cura_profile") }}">
|
||||
</span>
|
||||
<span class="add-on" data-bind="text: fileName"></span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ class DiscoveryPlugin(octoprint.plugin.StartupPlugin,
|
|||
if self._settings.get(["pathPrefix"]):
|
||||
path = self._settings.get(["pathPrefix"])
|
||||
else:
|
||||
prefix = self._settings.globalGet(["server", "reverseProxy", "prefixFallback"])
|
||||
prefix = self._settings.global_get(["server", "reverseProxy", "prefixFallback"])
|
||||
if prefix:
|
||||
path = prefix
|
||||
|
||||
|
|
@ -676,7 +676,7 @@ class DiscoveryPlugin(octoprint.plugin.StartupPlugin,
|
|||
return upnpUuid
|
||||
|
||||
def get_instance_name(self):
|
||||
name = self._settings.globalGet(["appearance", "name"])
|
||||
name = self._settings.global_get(["appearance", "name"])
|
||||
if name:
|
||||
return u"OctoPrint instance \"{}\"".format(name)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def getConnectionOptions():
|
|||
"autoconnect": settings().getBoolean(["serial", "autoconnect"])
|
||||
}
|
||||
|
||||
class Printer():
|
||||
class Printer(comm.MachineComPrintCallback):
|
||||
def __init__(self, fileManager, analysisQueue, printerProfileManager):
|
||||
from collections import deque
|
||||
|
||||
|
|
@ -329,7 +329,7 @@ class Printer():
|
|||
return
|
||||
|
||||
self._printAfterSelect = printAfterSelect
|
||||
self._comm.selectFile(filename, sd)
|
||||
self._comm.selectFile("/" + filename if sd else filename, sd)
|
||||
self._setProgressData(0, None, None, None)
|
||||
self._setCurrentZ(None)
|
||||
|
||||
|
|
@ -491,8 +491,14 @@ class Printer():
|
|||
|
||||
def _setJobData(self, filename, filesize, sd):
|
||||
if filename is not None:
|
||||
if sd:
|
||||
path_in_storage = filename[1:]
|
||||
path_on_disk = None
|
||||
else:
|
||||
path_in_storage = self._fileManager.path_in_storage(FileDestinations.LOCAL, filename)
|
||||
path_on_disk = self._fileManager.path_on_disk(FileDestinations.LOCAL, filename)
|
||||
self._selectedFile = {
|
||||
"filename": filename,
|
||||
"filename": path_in_storage,
|
||||
"filesize": filesize,
|
||||
"sd": sd,
|
||||
"estimatedPrintTime": None
|
||||
|
|
@ -518,14 +524,14 @@ class Printer():
|
|||
averagePrintTime = None
|
||||
date = None
|
||||
filament = None
|
||||
if filename:
|
||||
if path_on_disk:
|
||||
# Use a string for mtime because it could be float and the
|
||||
# javascript needs to exact match
|
||||
if not sd:
|
||||
date = int(os.stat(filename).st_ctime)
|
||||
date = int(os.stat(path_on_disk).st_ctime)
|
||||
|
||||
try:
|
||||
fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, filename)
|
||||
fileData = self._fileManager.get_metadata(FileDestinations.SDCARD if sd else FileDestinations.LOCAL, path_on_disk)
|
||||
except:
|
||||
fileData = None
|
||||
if fileData is not None:
|
||||
|
|
@ -549,7 +555,7 @@ class Printer():
|
|||
|
||||
self._stateMonitor.setJobData({
|
||||
"file": {
|
||||
"name": os.path.basename(filename) if filename is not None else None,
|
||||
"name": path_in_storage,
|
||||
"origin": FileDestinations.SDCARD if sd else FileDestinations.LOCAL,
|
||||
"size": filesize,
|
||||
"date": date
|
||||
|
|
@ -691,7 +697,7 @@ class Printer():
|
|||
def getSdFiles(self):
|
||||
if self._comm is None or not self._comm.isSdReady():
|
||||
return []
|
||||
return self._comm.getSdFiles()
|
||||
return map(lambda x: (x[0][1:], x[1]), self._comm.getSdFiles())
|
||||
|
||||
def addSdFile(self, filename, absolutePath, streamingFinishedCallback):
|
||||
if not self._comm or self._comm.isBusy() or not self._comm.isSdReady():
|
||||
|
|
@ -703,16 +709,16 @@ class Printer():
|
|||
self.refreshSdFiles(blocking=True)
|
||||
existingSdFiles = map(lambda x: x[0], self._comm.getSdFiles())
|
||||
|
||||
remoteName = util.getDosFilename(filename, existingSdFiles)
|
||||
remoteName = util.get_dos_filename(filename, existing_filenames=existingSdFiles, extension="gco")
|
||||
self._timeEstimationData = TimeEstimationHelper()
|
||||
self._comm.startFileTransfer(absolutePath, filename, remoteName)
|
||||
self._comm.startFileTransfer(absolutePath, filename, "/" + remoteName)
|
||||
|
||||
return remoteName
|
||||
|
||||
def deleteSdFile(self, filename):
|
||||
if not self._comm or not self._comm.isSdReady():
|
||||
return
|
||||
self._comm.deleteSdFile(filename)
|
||||
self._comm.deleteSdFile("/" + filename)
|
||||
|
||||
def initSdCard(self):
|
||||
if not self._comm or self._comm.isSdReady():
|
||||
|
|
@ -728,7 +734,7 @@ class Printer():
|
|||
"""
|
||||
Refreshs the list of file stored on the SD card attached to printer (if available and printer communication
|
||||
available). Optional blocking parameter allows making the method block (max 10s) until the file list has been
|
||||
received (and can be accessed via self._comm.getSdFiles()). Defaults to a asynchronous operation.
|
||||
received (and can be accessed via self._comm.getSdFiles()). Defaults to an asynchronous operation.
|
||||
"""
|
||||
if not self._comm or not self._comm.isSdReady():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -534,23 +534,33 @@ class Server():
|
|||
printer = Printer(fileManager, analysisQueue, printerProfileManager)
|
||||
appSessionManager = util.flask.AppSessionManager()
|
||||
|
||||
def plugin_settings_factory(name, implementation):
|
||||
def octoprint_plugin_inject_factory(name, implementation):
|
||||
if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin):
|
||||
return None
|
||||
return dict(
|
||||
plugin_manager=pluginManager,
|
||||
printer_profile_manager=printerProfileManager,
|
||||
event_bus=eventManager,
|
||||
analysis_queue=analysisQueue,
|
||||
slicing_manager=slicingManager,
|
||||
file_manager=fileManager,
|
||||
printer=printer,
|
||||
app_session_manager=appSessionManager
|
||||
)
|
||||
|
||||
def settings_plugin_inject_factory(name, implementation):
|
||||
if not isinstance(implementation, octoprint.plugin.SettingsPlugin):
|
||||
return None
|
||||
default_settings = implementation.get_settings_defaults()
|
||||
plugin_settings = octoprint.plugin.plugin_settings(name, defaults=default_settings)
|
||||
return dict(settings=plugin_settings)
|
||||
|
||||
pluginManager.initialize_implementations(additional_injects=dict(
|
||||
plugin_manager=pluginManager,
|
||||
printer_profile_manager=printerProfileManager,
|
||||
event_bus=eventManager,
|
||||
analysis_queue=analysisQueue,
|
||||
slicing_manager=slicingManager,
|
||||
file_manager=fileManager,
|
||||
printer=printer,
|
||||
app_session_manager=appSessionManager
|
||||
), additional_inject_factories=[plugin_settings_factory])
|
||||
pluginManager.initialize_implementations(
|
||||
additional_inject_factories=[
|
||||
octoprint_plugin_inject_factory,
|
||||
settings_plugin_inject_factory
|
||||
]
|
||||
)
|
||||
slicingManager.initialize()
|
||||
|
||||
# configure additional template folders for jinja2
|
||||
|
|
@ -581,7 +591,7 @@ class Server():
|
|||
if settings().getBoolean(["accessControl", "enabled"]):
|
||||
userManagerName = settings().get(["accessControl", "userManager"])
|
||||
try:
|
||||
clazz = octoprint.util.getClass(userManagerName)
|
||||
clazz = octoprint.util.get_class(userManagerName)
|
||||
userManager = clazz()
|
||||
except AttributeError, e:
|
||||
logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
|
||||
|
|
@ -784,6 +794,10 @@ class Server():
|
|||
|
||||
config = octoprint.util.dict_merge(defaultConfig, configFromFile)
|
||||
logging.config.dictConfig(config)
|
||||
logging.captureWarnings(True)
|
||||
|
||||
import warnings
|
||||
warnings.simplefilter("always")
|
||||
|
||||
if settings().getBoolean(["serial", "log"]):
|
||||
# enable debug logging to serial.log
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import octoprint.plugin
|
|||
from octoprint.server import admin_permission, NO_CONTENT
|
||||
from octoprint.settings import settings as s, valid_boolean_trues
|
||||
from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server.util.flask import restricted_access, get_remote_address, get_json_command_from_request
|
||||
|
||||
|
||||
#~~ init api blueprint, including sub modules
|
||||
|
|
@ -73,7 +73,7 @@ def pluginCommand(name):
|
|||
if valid_commands is None:
|
||||
return make_response("Method not allowed", 405)
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ def login():
|
|||
localNetworks.add(ip)
|
||||
|
||||
try:
|
||||
remoteAddr = util.getRemoteAddress(request)
|
||||
remoteAddr = get_remote_address(request)
|
||||
if netaddr.IPAddress(remoteAddr) in localNetworks:
|
||||
user = octoprint.server.userManager.findUser(autologinAs)
|
||||
if user is not None:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from octoprint.settings import settings
|
|||
from octoprint.printer import getConnectionOptions
|
||||
from octoprint.server import printer, printerProfileManager, NO_CONTENT
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
|
||||
import octoprint.util as util
|
||||
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ def connectionCommand():
|
|||
"disconnect": []
|
||||
}
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import octoprint.util as util
|
|||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
from octoprint.server import printer, fileManager, slicingManager, eventManager, NO_CONTENT
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
|
||||
from octoprint.server.api import api
|
||||
from octoprint.events import Events
|
||||
import octoprint.filemanager
|
||||
|
|
@ -27,7 +27,7 @@ def readGcodeFiles():
|
|||
filter = request.values["filter"]
|
||||
files = _getFileList(FileDestinations.LOCAL, filter=filter)
|
||||
files.extend(_getFileList(FileDestinations.SDCARD))
|
||||
return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads")))
|
||||
return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads")))
|
||||
|
||||
|
||||
@api.route("/files/<string:origin>", methods=["GET"])
|
||||
|
|
@ -38,7 +38,7 @@ def readGcodeFilesForOrigin(origin):
|
|||
files = _getFileList(origin)
|
||||
|
||||
if origin == FileDestinations.LOCAL:
|
||||
return jsonify(files=files, free=util.getFreeBytes(settings().getBaseFolder("uploads")))
|
||||
return jsonify(files=files, free=util.get_free_bytes(settings().getBaseFolder("uploads")))
|
||||
else:
|
||||
return jsonify(files=files)
|
||||
|
||||
|
|
@ -276,7 +276,7 @@ def gcodeFileCommand(filename, target):
|
|||
"slice": []
|
||||
}
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
from flask import request, make_response, jsonify
|
||||
|
||||
from octoprint.server import printer, NO_CONTENT
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
|
||||
from octoprint.server.api import api
|
||||
import octoprint.util as util
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ def controlJob():
|
|||
"cancel": []
|
||||
}
|
||||
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from octoprint.settings import settings
|
|||
from octoprint.server import NO_CONTENT, admin_permission
|
||||
from octoprint.server.util.flask import redirect_to_tornado, restricted_access
|
||||
from octoprint.server.api import api
|
||||
from octoprint.util import getFreeBytes
|
||||
from octoprint.util import get_free_bytes
|
||||
|
||||
|
||||
@api.route("/logs", methods=["GET"])
|
||||
|
|
@ -23,7 +23,7 @@ from octoprint.util import getFreeBytes
|
|||
@admin_permission.require(403)
|
||||
def getLogFiles():
|
||||
files = _getLogFiles()
|
||||
return jsonify(files=files, free=getFreeBytes(settings().getBaseFolder("logs")))
|
||||
return jsonify(files=files, free=get_free_bytes(settings().getBaseFolder("logs")))
|
||||
|
||||
|
||||
@api.route("/logs/<path:filename>", methods=["GET"])
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import re
|
|||
from octoprint.settings import settings, valid_boolean_trues
|
||||
from octoprint.server import printer, NO_CONTENT
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
|
||||
import octoprint.util as util
|
||||
|
||||
#~~ Printer
|
||||
|
|
@ -64,7 +64,7 @@ def printerToolCommand():
|
|||
"extrude": ["amount"],
|
||||
"flowrate": ["factor"]
|
||||
}
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ def printerBedCommand():
|
|||
"target": ["target"],
|
||||
"offset": ["offset"]
|
||||
}
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ def printerPrintheadCommand():
|
|||
"home": ["axes"],
|
||||
"feedrate": ["factor"]
|
||||
}
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ def printerSdCommand():
|
|||
"refresh": [],
|
||||
"release": []
|
||||
}
|
||||
command, data, response = util.getJsonCommandFromRequest(request, valid_commands)
|
||||
command, data, response = get_json_command_from_request(request, valid_commands)
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ def getSettings():
|
|||
"snapshotUrl": s.get(["webcam", "snapshot"]),
|
||||
"ffmpegPath": s.get(["webcam", "ffmpeg"]),
|
||||
"bitrate": s.get(["webcam", "bitrate"]),
|
||||
"ffmpegThreads": s.get(["webcam", "ffmpegThreads"]),
|
||||
"watermark": s.getBoolean(["webcam", "watermark"]),
|
||||
"flipH": s.getBoolean(["webcam", "flipH"]),
|
||||
"flipV": s.getBoolean(["webcam", "flipV"])
|
||||
|
|
@ -139,6 +140,7 @@ def setSettings():
|
|||
if "snapshotUrl" in data["webcam"].keys(): s.set(["webcam", "snapshot"], data["webcam"]["snapshotUrl"])
|
||||
if "ffmpegPath" in data["webcam"].keys(): s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"])
|
||||
if "bitrate" in data["webcam"].keys(): s.set(["webcam", "bitrate"], data["webcam"]["bitrate"])
|
||||
if "ffmpegThreads" in data["webcam"].keys(): s.setInt(["webcam", "ffmpegThreads"], data["webcam"]["ffmpegThreads"])
|
||||
if "watermark" in data["webcam"].keys(): s.setBoolean(["webcam", "watermark"], data["webcam"]["watermark"])
|
||||
if "flipH" in data["webcam"].keys(): s.setBoolean(["webcam", "flipH"], data["webcam"]["flipH"])
|
||||
if "flipV" in data["webcam"].keys(): s.setBoolean(["webcam", "flipV"], data["webcam"]["flipV"])
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
|
||||
import os
|
||||
|
||||
from flask import request, jsonify, url_for
|
||||
from flask import request, jsonify, url_for, make_response
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import octoprint.timelapse
|
||||
|
|
@ -30,9 +30,11 @@ def getTimelapseData():
|
|||
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
|
||||
config["type"] = "zchange"
|
||||
config["postRoll"] = timelapse.postRoll()
|
||||
config["fps"] = timelapse.fps()
|
||||
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
|
||||
config["type"] = "timed"
|
||||
config["postRoll"] = timelapse.postRoll()
|
||||
config["fps"] = timelapse.fps()
|
||||
config.update({
|
||||
"interval": timelapse.interval()
|
||||
})
|
||||
|
|
@ -55,7 +57,7 @@ def downloadTimelapse(filename):
|
|||
@api.route("/timelapse/<filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteTimelapse(filename):
|
||||
if util.isAllowedFile(filename, {"mpg"}):
|
||||
if util.is_allowed_file(filename, {"mpg"}):
|
||||
timelapse_folder = settings().getBaseFolder("timelapse")
|
||||
full_path = os.path.realpath(os.path.join(timelapse_folder, filename))
|
||||
if full_path.startswith(timelapse_folder) and os.path.exists(full_path):
|
||||
|
|
@ -70,26 +72,46 @@ def setTimelapseConfig():
|
|||
config = {
|
||||
"type": request.values["type"],
|
||||
"postRoll": 0,
|
||||
"fps": 25,
|
||||
"options": {}
|
||||
}
|
||||
|
||||
|
||||
if "postRoll" in request.values:
|
||||
try:
|
||||
config["postRoll"] = int(request.values["postRoll"])
|
||||
postRoll = int(request.values["postRoll"])
|
||||
except ValueError:
|
||||
pass
|
||||
return make_response("Invalid value for postRoll: %r" % request.values["postRoll"], 400)
|
||||
else:
|
||||
if postRoll >= 0:
|
||||
config["postRoll"] = postRoll
|
||||
else:
|
||||
return make_response("Invalid value for postRoll: %d" % postRoll, 400)
|
||||
|
||||
if "fps" in request.values:
|
||||
try:
|
||||
fps = int(request.values["fps"])
|
||||
except ValueError:
|
||||
return make_response("Invalid value for fps: %r" % request.values["fps"], 400)
|
||||
else:
|
||||
if fps > 0:
|
||||
config["fps"] = fps
|
||||
else:
|
||||
return make_response("Invalid value for fps: %d" % fps, 400)
|
||||
|
||||
if "interval" in request.values:
|
||||
interval = 10
|
||||
config["options"] = {
|
||||
"interval": 10
|
||||
}
|
||||
|
||||
try:
|
||||
interval = int(request.values["interval"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
config["options"] = {
|
||||
"interval": interval
|
||||
}
|
||||
return make_response("Invalid value for interval: %r" % request.values["interval"])
|
||||
else:
|
||||
if interval > 0:
|
||||
config["options"]["interval"] = interval
|
||||
else:
|
||||
return make_response("Invalid value for interval: %d" % interval)
|
||||
|
||||
if admin_permission.can() and "save" in request.values and request.values["save"] in valid_boolean_trues:
|
||||
octoprint.timelapse.configureTimelapse(config, True)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
from flask import make_response
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
|
@ -245,3 +246,25 @@ class AppSessionManager(object):
|
|||
|
||||
self._logger.debug("App sessions after cleanup: %r" % self._sessions)
|
||||
|
||||
|
||||
def get_remote_address(request):
|
||||
forwardedFor = request.headers.get("X-Forwarded-For", None)
|
||||
if forwardedFor is not None:
|
||||
return forwardedFor.split(",")[0]
|
||||
return request.remote_addr
|
||||
|
||||
|
||||
def get_json_command_from_request(request, valid_commands):
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return None, None, make_response("Expected content-type JSON", 400)
|
||||
|
||||
data = request.json
|
||||
if not "command" in data.keys() or not data["command"] in valid_commands.keys():
|
||||
return None, None, make_response("Expected valid command", 400)
|
||||
|
||||
command = data["command"]
|
||||
for parameter in valid_commands[command]:
|
||||
if not parameter in data:
|
||||
return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400)
|
||||
|
||||
return command, data, None
|
||||
|
|
@ -373,7 +373,7 @@ class UploadStorageFallbackHandler(tornado.web.RequestHandler):
|
|||
finally:
|
||||
# make sure the temporary files are removed again
|
||||
for f in self._files:
|
||||
octoprint.util.silentRemove(f)
|
||||
octoprint.util.silent_remove(f)
|
||||
|
||||
# make all http methods trigger _handle_method
|
||||
get = _handle_method
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
|
|||
self.filename = os.path.basename(self._path)
|
||||
|
||||
def save(self, target):
|
||||
octoprint.util.safeRename(self._path, target)
|
||||
octoprint.util.safe_rename(self._path, target)
|
||||
|
||||
file_wrapper = WatchdogFileWrapper(path)
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ default_settings = {
|
|||
"stream": None,
|
||||
"snapshot": None,
|
||||
"ffmpeg": None,
|
||||
"ffmpegThreads": 1,
|
||||
"bitrate": "5000k",
|
||||
"watermark": True,
|
||||
"flipH": False,
|
||||
|
|
@ -68,7 +69,8 @@ default_settings = {
|
|||
"timelapse": {
|
||||
"type": "off",
|
||||
"options": {},
|
||||
"postRoll": 0
|
||||
"postRoll": 0,
|
||||
"fps": 25
|
||||
}
|
||||
},
|
||||
"gcodeViewer": {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -236,6 +236,26 @@ $(function() {
|
|||
return $(window).height() - 165;
|
||||
};
|
||||
|
||||
// jquery plugin to select all text in an element
|
||||
// originally from: http://stackoverflow.com/a/987376
|
||||
$.fn.selectText = function() {
|
||||
var doc = document;
|
||||
var element = this[0];
|
||||
var range, selection;
|
||||
|
||||
if (doc.body.createTextRange) {
|
||||
range = document.body.createTextRange();
|
||||
range.moveToElementText(element);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
selection = window.getSelection();
|
||||
range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
};
|
||||
|
||||
// Use bootstrap tabdrop for tabs and pills
|
||||
$('.nav-pills, .nav-tabs').tabdrop();
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ $(function() {
|
|||
self.webcam_snapshotUrl = ko.observable(undefined);
|
||||
self.webcam_ffmpegPath = ko.observable(undefined);
|
||||
self.webcam_bitrate = ko.observable(undefined);
|
||||
self.webcam_ffmpegThreads = ko.observable(undefined);
|
||||
self.webcam_watermark = ko.observable(undefined);
|
||||
self.webcam_flipH = ko.observable(undefined);
|
||||
self.webcam_flipV = ko.observable(undefined);
|
||||
|
|
@ -188,6 +189,7 @@ $(function() {
|
|||
self.webcam_snapshotUrl(response.webcam.snapshotUrl);
|
||||
self.webcam_ffmpegPath(response.webcam.ffmpegPath);
|
||||
self.webcam_bitrate(response.webcam.bitrate);
|
||||
self.webcam_ffmpegThreads(response.webcam.ffmpegThreads);
|
||||
self.webcam_watermark(response.webcam.watermark);
|
||||
self.webcam_flipH(response.webcam.flipH);
|
||||
self.webcam_flipV(response.webcam.flipV);
|
||||
|
|
@ -255,6 +257,7 @@ $(function() {
|
|||
"snapshotUrl": self.webcam_snapshotUrl(),
|
||||
"ffmpegPath": self.webcam_ffmpegPath(),
|
||||
"bitrate": self.webcam_bitrate(),
|
||||
"ffmpegThreads": self.webcam_ffmpegThreads(),
|
||||
"watermark": self.webcam_watermark(),
|
||||
"flipH": self.webcam_flipH(),
|
||||
"flipV": self.webcam_flipV()
|
||||
|
|
@ -326,4 +329,4 @@ $(function() {
|
|||
["loginStateViewModel", "usersViewModel", "printerProfilesViewModel"],
|
||||
["#settings_dialog", "#navbar_settings"]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ $(function() {
|
|||
self.loginState = parameters[0];
|
||||
self.settings = parameters[1];
|
||||
|
||||
self.log = [];
|
||||
self.log = ko.observableArray([]);
|
||||
self.buffer = ko.observable(300);
|
||||
|
||||
self.command = ko.observable(undefined);
|
||||
|
||||
|
|
@ -20,15 +21,56 @@ $(function() {
|
|||
self.autoscrollEnabled = ko.observable(true);
|
||||
|
||||
self.filters = self.settings.terminalFilters;
|
||||
self.filterRegex = undefined;
|
||||
self.filterRegex = ko.observable();
|
||||
|
||||
self.cmdHistory = [];
|
||||
self.cmdHistoryIdx = -1;
|
||||
|
||||
self.displayedLines = ko.computed(function() {
|
||||
var regex = self.filterRegex();
|
||||
var lineVisible = function(entry) {
|
||||
return regex == undefined || !entry.line.match(regex);
|
||||
};
|
||||
|
||||
var filtered = false;
|
||||
var result = [];
|
||||
_.each(self.log(), function(entry) {
|
||||
if (lineVisible(entry)) {
|
||||
result.push(entry);
|
||||
filtered = false;
|
||||
} else if (!filtered) {
|
||||
result.push(self._toInternalFormat("[...]", "filtered"));
|
||||
filtered = true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
self.displayedLines.subscribe(function() {
|
||||
self.updateOutput();
|
||||
});
|
||||
|
||||
self.lineCount = ko.computed(function() {
|
||||
var total = self.log().length;
|
||||
var displayed = _.filter(self.displayedLines(), function(entry) { return entry.type == "line" }).length;
|
||||
var filtered = total - displayed;
|
||||
|
||||
if (total == displayed) {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
|
||||
} else {
|
||||
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
|
||||
}
|
||||
});
|
||||
|
||||
self.autoscrollEnabled.subscribe(function(newValue) {
|
||||
if (newValue) {
|
||||
self.log(self.log.slice(-self.buffer()));
|
||||
}
|
||||
});
|
||||
|
||||
self.activeFilters = ko.observableArray([]);
|
||||
self.activeFilters.subscribe(function(e) {
|
||||
self.updateFilterRegex();
|
||||
self.updateOutput();
|
||||
});
|
||||
|
||||
self.fromCurrentData = function(data) {
|
||||
|
|
@ -42,16 +84,21 @@ $(function() {
|
|||
};
|
||||
|
||||
self._processCurrentLogData = function(data) {
|
||||
if (!self.log)
|
||||
self.log = [];
|
||||
self.log = self.log.concat(data);
|
||||
self.log = self.log.slice(-300);
|
||||
self.updateOutput();
|
||||
self.log(self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) })));
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.log(self.log.slice(-300));
|
||||
}
|
||||
};
|
||||
|
||||
self._processHistoryLogData = function(data) {
|
||||
self.log = data;
|
||||
self.updateOutput();
|
||||
self.log(_.map(data, function(line) { return self._toInternalFormat(line) }));
|
||||
};
|
||||
|
||||
self._toInternalFormat = function(line, type) {
|
||||
if (type == undefined) {
|
||||
type = "line";
|
||||
}
|
||||
return {line: line, type: type}
|
||||
};
|
||||
|
||||
self._processStateData = function(data) {
|
||||
|
|
@ -67,29 +114,34 @@ $(function() {
|
|||
self.updateFilterRegex = function() {
|
||||
var filterRegexStr = self.activeFilters().join("|").trim();
|
||||
if (filterRegexStr == "") {
|
||||
self.filterRegex = undefined;
|
||||
self.filterRegex(undefined);
|
||||
} else {
|
||||
self.filterRegex = new RegExp(filterRegexStr);
|
||||
self.filterRegex(new RegExp(filterRegexStr));
|
||||
}
|
||||
self.updateOutput();
|
||||
};
|
||||
|
||||
self.updateOutput = function() {
|
||||
if (!self.log)
|
||||
return;
|
||||
|
||||
var output = "";
|
||||
for (var i = 0; i < self.log.length; i++) {
|
||||
if (self.filterRegex !== undefined && self.log[i].match(self.filterRegex)) continue;
|
||||
output += self.log[i] + "\n";
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.scrollToEnd();
|
||||
}
|
||||
};
|
||||
|
||||
self.toggleAutoscroll = function() {
|
||||
self.autoscrollEnabled(!self.autoscrollEnabled());
|
||||
};
|
||||
|
||||
self.selectAll = function() {
|
||||
var container = $("#terminal-output");
|
||||
if (container.length) {
|
||||
container.text(output);
|
||||
container.selectText();
|
||||
}
|
||||
};
|
||||
|
||||
if (self.autoscrollEnabled()) {
|
||||
container.scrollTop(container[0].scrollHeight - container.height())
|
||||
}
|
||||
self.scrollToEnd = function() {
|
||||
var container = $("#terminal-output");
|
||||
if (container.length) {
|
||||
container.scrollTop(container[0].scrollHeight - container.height())
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -155,10 +207,12 @@ $(function() {
|
|||
};
|
||||
|
||||
self.onAfterTabChange = function(current, previous) {
|
||||
if (current != "#terminal") {
|
||||
if (current != "#term") {
|
||||
return;
|
||||
}
|
||||
self.updateOutput();
|
||||
if (self.autoscrollEnabled()) {
|
||||
self.scrollToEnd();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@ $(function() {
|
|||
|
||||
self.loginState = parameters[0];
|
||||
|
||||
self.defaultFps = 25;
|
||||
self.defaultPostRoll = 0;
|
||||
self.defaultInterval = 10;
|
||||
|
||||
self.timelapseType = ko.observable(undefined);
|
||||
self.timelapseTimedInterval = ko.observable(undefined);
|
||||
self.timelapsePostRoll = ko.observable(undefined);
|
||||
self.timelapseTimedInterval = ko.observable(self.defaultInterval);
|
||||
self.timelapsePostRoll = ko.observable(self.defaultPostRoll);
|
||||
self.timelapseFps = ko.observable(self.defaultFps);
|
||||
|
||||
self.persist = ko.observable(false);
|
||||
self.isDirty = ko.observable(false);
|
||||
|
|
@ -42,6 +47,9 @@ $(function() {
|
|||
self.timelapsePostRoll.subscribe(function(newValue) {
|
||||
self.isDirty(true);
|
||||
});
|
||||
self.timelapseFps.subscribe(function(newValue) {
|
||||
self.isDirty(true);
|
||||
});
|
||||
|
||||
// initialize list helper
|
||||
self.listHelper = new ItemListHelper(
|
||||
|
|
@ -91,15 +99,23 @@ $(function() {
|
|||
self.listHelper.updateItems(response.files);
|
||||
|
||||
if (config.type == "timed") {
|
||||
if (response.config.interval != undefined && response.config.interval > 0) {
|
||||
self.timelapseTimedInterval(response.config.interval);
|
||||
}
|
||||
if (response.config.postRoll != undefined && response.config.postRoll >= 0) {
|
||||
self.timelapsePostRoll(response.config.postRoll);
|
||||
if (config.interval != undefined && config.interval > 0) {
|
||||
self.timelapseTimedInterval(config.interval);
|
||||
}
|
||||
} else {
|
||||
self.timelapseTimedInterval(undefined);
|
||||
self.timelapsePostRoll(undefined);
|
||||
self.timelapseTimedInterval(self.defaultInterval);
|
||||
}
|
||||
|
||||
if (config.postRoll != undefined && config.postRoll >= 0) {
|
||||
self.timelapsePostRoll(config.postRoll);
|
||||
} else {
|
||||
self.timelapsePostRoll(self.defaultPostRoll);
|
||||
}
|
||||
|
||||
if (config.fps != undefined && config.fps > 0) {
|
||||
self.timelapseFps(config.fps);
|
||||
} else {
|
||||
self.timelapseFps(self.defaultFps);
|
||||
}
|
||||
|
||||
self.persist(false);
|
||||
|
|
@ -137,6 +153,7 @@ $(function() {
|
|||
var payload = {
|
||||
"type": self.timelapseType(),
|
||||
"postRoll": self.timelapsePostRoll(),
|
||||
"fps": self.timelapseFps(),
|
||||
"save": self.persist()
|
||||
};
|
||||
|
||||
|
|
@ -167,4 +184,4 @@ $(function() {
|
|||
["loginStateViewModel"],
|
||||
"#timelapse"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ table {
|
|||
}
|
||||
|
||||
#temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support,
|
||||
#webcam_timelapse_interval, #webcam_timelapse_postRoll {
|
||||
#webcam_timelapse_interval, #webcam_timelapse_postRoll, #webcam_timelapse_fps {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
|
@ -590,8 +590,17 @@ ul.dropdown-menu li a {
|
|||
/** Terminal output */
|
||||
|
||||
#term {
|
||||
#terminal-output {
|
||||
min-height: 340px;
|
||||
.terminal {
|
||||
#terminal-output {
|
||||
min-height: 340px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
#terminal-sendpanel {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,34 @@
|
|||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<div class="control-group" title="{{ _('URL to embed into the UI for live viewing of the webcam stream') }}">
|
||||
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Stream URL') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_streamUrl" id="settings-webcamStreamUrl">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Snapshot URL') }}</label>
|
||||
<div class="control-group" title="{{ _('URL to use for retrieving webcam snapshot images for timelapse creation') }}">
|
||||
<label class="control-label" for="settings-webcamSnapshotUrl">{{ _('Snapshot URL') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_snapshotUrl" id="settings-webcamSnapshotUrl">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Path to FFMPEG') }}</label>
|
||||
<div class="control-group" title="{{ _('Full path to the FFMPEG binary') }}">
|
||||
<label class="control-label" for="settings-webcamFfmpegPath">{{ _('Path to FFMPEG') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_ffmpegPath" id="settings-webcamFfmpegPath">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="control-group" title="{{ _('Bitrate to use for encoding the timelapse video') }}">
|
||||
<label class="control-label" for="settings-webcamBitrate">{{ _('Timelapse bitrate') }}</label>
|
||||
<div class="controls">
|
||||
<input type="text" class="input-block-level" data-bind="value: webcam_bitrate" id="settings-webcamBitrate">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="{{ _('Number of FFMPEG encoding threads') }}">
|
||||
<label class="control-label" for="settings-webcam_ffmpegThreads">{{ _('FFMPEG threads') }}</label>
|
||||
<div class="controls">
|
||||
<input class="input-block-level" data-bind="value: webcam_ffmpegThreads" id="settings-webcamFfmpegThreads" type="number" step="1" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<label class="checkbox">
|
||||
|
|
|
|||
|
|
@ -60,6 +60,6 @@
|
|||
<div class="bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div>
|
||||
<small>{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
|
||||
<small class="muted">{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: keycontrolPossible">
|
||||
<small>{{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}</small>
|
||||
<small class="muted">{{ _("Hint: If you move your mouse over the picture, you enter keyboard control mode.") }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
<pre id="terminal-output" class="pre-scrollable"></pre>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> {{ _('Autoscroll') }}
|
||||
</label>
|
||||
<div data-bind="foreach: filters">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
|
||||
</label>
|
||||
<div class="terminal">
|
||||
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered'}"></span><br></pre>
|
||||
<small class="pull-left"><button class="btn btn-mini" data-bind="click: toggleAutoscroll, css: {active: autoscrollEnabled}">{{ _('Autoscroll') }}</button> <span data-bind="text: lineCount"></span></small>
|
||||
<small class="pull-right"><a href="#" data-bind="click: scrollToEnd">{{ _("Scroll to end") }}</a> | <a href="#" data-bind="click: selectAll">{{ _("Select all") }}</a></small>
|
||||
</div>
|
||||
|
||||
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
|
||||
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
|
||||
<div class="row-fluid">
|
||||
<div class="span6" id="termin-filterpanel">
|
||||
<div data-bind="foreach: filters">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-bind="attr: { value: regex }, checked: $parent.activeFilters"> <span data-bind="text: name"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span6" id="terminal-sendpanel" style="display: none;" data-bind="visible: loginState.isUser">
|
||||
<div class="input-append">
|
||||
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
|
||||
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
|
||||
</div>
|
||||
<small class="muted">{{ _('Hint: Use the arrow up/down keys to recall commands sent previously') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
<option value="timed">{{ _('Timed') }}</option>
|
||||
</select>
|
||||
|
||||
<label for="webcam_timelapse_fps">{{ _('Timelapse frame rate (in frames per second)') }}</label>
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-mini" id="webcam_timelapse_fps" data-bind="value: timelapseFps, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
|
||||
<span class="add-on">{{ _('fps') }}</span>
|
||||
</div>
|
||||
|
||||
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ def getFinishedTimelapses():
|
|||
statResult = os.stat(os.path.join(basedir, osFile))
|
||||
files.append({
|
||||
"name": osFile,
|
||||
"size": util.getFormattedSize(statResult.st_size),
|
||||
"size": util.get_formatted_size(statResult.st_size),
|
||||
"bytes": statResult.st_size,
|
||||
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
|
||||
"date": util.get_formatted_datetime(datetime.datetime.fromtimestamp(statResult.st_ctime))
|
||||
})
|
||||
return files
|
||||
|
||||
|
|
@ -76,18 +76,22 @@ def configureTimelapse(config=None, persist=False):
|
|||
type = config["type"]
|
||||
|
||||
postRoll = 0
|
||||
if "postRoll" in config:
|
||||
if "postRoll" in config and config["postRoll"] >= 0:
|
||||
postRoll = config["postRoll"]
|
||||
|
||||
fps = 25
|
||||
if "fps" in config and config["fps"] > 0:
|
||||
fps = config["fps"]
|
||||
|
||||
if type is None or "off" == type:
|
||||
current = None
|
||||
elif "zchange" == type:
|
||||
current = ZTimelapse(postRoll=postRoll)
|
||||
current = ZTimelapse(postRoll=postRoll, fps=fps)
|
||||
elif "timed" == type:
|
||||
interval = 10
|
||||
if "options" in config and "interval" in config["options"]:
|
||||
if "options" in config and "interval" in config["options"] and config["options"]["interval"] > 0:
|
||||
interval = config["options"]["interval"]
|
||||
current = TimedTimelapse(postRoll=postRoll, interval=interval)
|
||||
current = TimedTimelapse(postRoll=postRoll, interval=interval, fps=fps)
|
||||
|
||||
notifyCallbacks(current)
|
||||
|
||||
|
|
@ -97,7 +101,7 @@ def configureTimelapse(config=None, persist=False):
|
|||
|
||||
|
||||
class Timelapse(object):
|
||||
def __init__(self, postRoll=0):
|
||||
def __init__(self, postRoll=0, fps=25):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._imageNumber = None
|
||||
self._inTimelapse = False
|
||||
|
|
@ -110,8 +114,9 @@ class Timelapse(object):
|
|||
self._captureDir = settings().getBaseFolder("timelapse_tmp")
|
||||
self._movieDir = settings().getBaseFolder("timelapse")
|
||||
self._snapshotUrl = settings().get(["webcam", "snapshot"])
|
||||
self._ffmpegThreads = settings().get(["webcam", "ffmpegThreads"])
|
||||
|
||||
self._fps = 25
|
||||
self._fps = fps
|
||||
|
||||
self._renderThread = None
|
||||
self._captureMutex = threading.Lock()
|
||||
|
|
@ -127,6 +132,9 @@ class Timelapse(object):
|
|||
def postRoll(self):
|
||||
return self._postRoll
|
||||
|
||||
def fps(self):
|
||||
return self._fps
|
||||
|
||||
def unload(self):
|
||||
if self._inTimelapse:
|
||||
self.stopTimelapse(doCreateMovie=False)
|
||||
|
|
@ -267,7 +275,7 @@ class Timelapse(object):
|
|||
|
||||
# prepare ffmpeg command
|
||||
command = [
|
||||
ffmpeg, '-loglevel', 'error', '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b:v', bitrate,
|
||||
ffmpeg, '-framerate', str(self._fps), '-loglevel', 'error', '-i', input, '-vcodec', 'mpeg2video', '-threads', str(self._ffmpegThreads), '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b', bitrate,
|
||||
'-f', 'vob']
|
||||
|
||||
filters = []
|
||||
|
|
@ -335,8 +343,8 @@ class Timelapse(object):
|
|||
|
||||
|
||||
class ZTimelapse(Timelapse):
|
||||
def __init__(self, postRoll=0):
|
||||
Timelapse.__init__(self, postRoll=postRoll)
|
||||
def __init__(self, postRoll=0, fps=25):
|
||||
Timelapse.__init__(self, postRoll=postRoll, fps=fps)
|
||||
self._logger.debug("ZTimelapse initialized")
|
||||
|
||||
def eventSubscriptions(self):
|
||||
|
|
@ -370,8 +378,8 @@ class ZTimelapse(Timelapse):
|
|||
|
||||
|
||||
class TimedTimelapse(Timelapse):
|
||||
def __init__(self, postRoll=0, interval=1):
|
||||
Timelapse.__init__(self, postRoll=postRoll)
|
||||
def __init__(self, postRoll=0, interval=1, fps=25):
|
||||
Timelapse.__init__(self, postRoll=postRoll, fps=fps)
|
||||
self._interval = interval
|
||||
if self._interval < 1:
|
||||
self._interval = 1 # force minimum interval of 1s
|
||||
|
|
|
|||
|
|
@ -1,38 +1,135 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
This module bundles commonly used utility methods or helper classes that are used in multiple places withing
|
||||
OctoPrint's source code.
|
||||
"""
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
import tempfile
|
||||
import logging
|
||||
import shutil
|
||||
from flask import make_response
|
||||
|
||||
from octoprint.settings import settings, default_settings
|
||||
from functools import wraps
|
||||
import warnings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def getFormattedSize(num):
|
||||
def warning_decorator_factory(warning_type):
|
||||
def specific_warning(message, stacklevel=1, since=None, includedoc=None):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def func_wrapper(*args, **kwargs):
|
||||
# we need to increment the stacklevel by one because otherwise we'll get the location of our
|
||||
# func_wrapper in the log, instead of our caller (which is the real caller of the wrapped function)
|
||||
warnings.warn(message, warning_type, stacklevel=stacklevel + 1)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
if includedoc is not None and since is not None:
|
||||
docstring = "\n.. deprecated:: {since}\n {message}\n\n".format(since=since, message=includedoc)
|
||||
if hasattr(func_wrapper, "__doc__") and func_wrapper.__doc__ is not None:
|
||||
docstring = func_wrapper.__doc__ + "\n" + docstring
|
||||
func_wrapper.__doc__ = docstring
|
||||
|
||||
return func_wrapper
|
||||
|
||||
return decorator
|
||||
return specific_warning
|
||||
|
||||
deprecated = warning_decorator_factory(DeprecationWarning)
|
||||
"""
|
||||
A decorator for deprecated methods. Logs a deprecation warning via Python's `:mod:`warnings` module including the
|
||||
supplied ``message``. The call stack level used (for adding the source location of the offending call to the
|
||||
warning) can be overridden using the optional ``stacklevel`` parameter. If both ``since`` and ``includedoc`` are
|
||||
provided, a deprecation warning will also be added to the function's docstring by providing or extending its ``__doc__``
|
||||
property.
|
||||
|
||||
Arguments:
|
||||
message (string): The message to include in the deprecation warning.
|
||||
stacklevel (int): Stack level for including the caller of the offending method in the logged warning. Defaults to 1,
|
||||
meaning the direct caller of the method. It might make sense to increase this in case of the function call
|
||||
happening dynamically from a fixed position to not shadow the real caller (e.g. in case of overridden
|
||||
``getattr`` methods).
|
||||
includedoc (string): Message about the deprecation to include in the wrapped function's docstring.
|
||||
since (string): Version since when the function was deprecated, must be present for the docstring to get extended.
|
||||
|
||||
Returns:
|
||||
function: The wrapped function with the deprecation warnings in place.
|
||||
"""
|
||||
|
||||
pending_deprecation = warning_decorator_factory(PendingDeprecationWarning)
|
||||
"""
|
||||
A decorator for methods pending deprecation. Logs a pending deprecation warning via Python's `:mod:`warnings` module
|
||||
including the supplied ``message``. The call stack level used (for adding the source location of the offending call to
|
||||
the warning) can be overridden using the optional ``stacklevel`` parameter. If both ``since`` and ``includedoc`` are
|
||||
provided, a deprecation warning will also be added to the function's docstring by providing or extending its ``__doc__``
|
||||
property.
|
||||
|
||||
Arguments:
|
||||
message (string): The message to include in the deprecation warning.
|
||||
stacklevel (int): Stack level for including the caller of the offending method in the logged warning. Defaults to 1,
|
||||
meaning the direct caller of the method. It might make sense to increase this in case of the function call
|
||||
happening dynamically from a fixed position to not shadow the real caller (e.g. in case of overridden
|
||||
``getattr`` methods).
|
||||
includedoc (string): Message about the deprecation to include in the wrapped function's docstring.
|
||||
since (string): Version since when the function was deprecated, must be present for the docstring to get extended.
|
||||
|
||||
Returns:
|
||||
function: The wrapped function with the deprecation warnings in place.
|
||||
"""
|
||||
|
||||
def get_formatted_size(num):
|
||||
"""
|
||||
Taken from http://stackoverflow.com/a/1094933/2028598
|
||||
Formats the given byte count as a human readable rounded size expressed in the most pressing unit among B(ytes),
|
||||
K(ilo)B(ytes), M(ega)B(ytes), G(iga)B(ytes) and T(era)B(ytes), with one decimal place.
|
||||
|
||||
Based on http://stackoverflow.com/a/1094933/2028598
|
||||
|
||||
Arguments:
|
||||
num (int): The byte count to format
|
||||
|
||||
Returns:
|
||||
string: The formatted byte count.
|
||||
"""
|
||||
for x in ["bytes","KB","MB","GB"]:
|
||||
|
||||
for x in ["B","KB","MB","GB"]:
|
||||
if num < 1024.0:
|
||||
return "%3.1f%s" % (num, x)
|
||||
num /= 1024.0
|
||||
return "%3.1f%s" % (num, "TB")
|
||||
|
||||
|
||||
def isAllowedFile(filename, extensions):
|
||||
return "." in filename and filename.rsplit(".", 1)[1] in extensions
|
||||
def is_allowed_file(filename, extensions):
|
||||
"""
|
||||
Determines if the provided ``filename`` has one of the supplied ``extensions``. The check is done case-insensitive.
|
||||
|
||||
Arguments:
|
||||
filename (string): The file name to check against the extensions.
|
||||
extensions (list): The extensions to check against, a list of strings
|
||||
|
||||
Return:
|
||||
boolean: True if the file name's extension matches one of the allowed extensions, False otherwise.
|
||||
"""
|
||||
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in map(str.lower, extensions)
|
||||
|
||||
|
||||
def getFormattedTimeDelta(d):
|
||||
def get_formatted_timedelta(d):
|
||||
"""
|
||||
Formats a timedelta instance as "HH:MM:ss" and returns the resulting string.
|
||||
|
||||
Arguments:
|
||||
d (datetime.timedelta): The timedelta instance to format
|
||||
|
||||
Returns:
|
||||
string: The timedelta formatted as "HH:MM:ss"
|
||||
"""
|
||||
|
||||
if d is None:
|
||||
return None
|
||||
hours = d.days * 24 + d.seconds // 3600
|
||||
|
|
@ -41,17 +138,39 @@ def getFormattedTimeDelta(d):
|
|||
return "%02d:%02d:%02d" % (hours, minutes, seconds)
|
||||
|
||||
|
||||
def getFormattedDateTime(d):
|
||||
def get_formatted_datetime(d):
|
||||
"""
|
||||
Formats a datetime instance as "YYYY-mm-dd HH:MM" and returns the resulting string.
|
||||
|
||||
Arguments:
|
||||
d (datetime.datetime): The datetime instance to format
|
||||
|
||||
Returns:
|
||||
string: The datetime formatted as "YYYY-mm-dd HH:MM"
|
||||
"""
|
||||
|
||||
if d is None:
|
||||
return None
|
||||
|
||||
return d.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def getClass(name):
|
||||
def get_class(name):
|
||||
"""
|
||||
Taken from http://stackoverflow.com/a/452981/2028598
|
||||
Retrieves the class object for a given fully qualified class name.
|
||||
|
||||
Taken from http://stackoverflow.com/a/452981/2028598.
|
||||
|
||||
Arguments:
|
||||
name (string): The fully qualified class name, including all modules separated by ``.``
|
||||
|
||||
Returns:
|
||||
type: The class if it could be found.
|
||||
|
||||
Raises:
|
||||
AttributeError: The class could not be found.
|
||||
"""
|
||||
|
||||
parts = name.split(".")
|
||||
module = ".".join(parts[:-1])
|
||||
m = __import__(module)
|
||||
|
|
@ -60,50 +179,34 @@ def getClass(name):
|
|||
return m
|
||||
|
||||
|
||||
def isDevVersion():
|
||||
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../../.git"))
|
||||
return os.path.exists(gitPath)
|
||||
def get_exception_string():
|
||||
"""
|
||||
Retrieves the exception info of the last raised exception and returns it as a string formatted as
|
||||
``<exception type>: <exception message> @ <source file>:<function name>:<line number>'.
|
||||
|
||||
Returns:
|
||||
string: The formatted exception information.
|
||||
"""
|
||||
|
||||
def getExceptionString():
|
||||
locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
|
||||
return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])
|
||||
|
||||
|
||||
def getGitInfo():
|
||||
gitPath = os.path.abspath(os.path.join(os.path.split(os.path.abspath(__file__))[0], "../../../.git"))
|
||||
if not os.path.exists(gitPath):
|
||||
return (None, None)
|
||||
|
||||
headref = None
|
||||
with open(os.path.join(gitPath, "HEAD"), "r") as f:
|
||||
headref = f.readline().strip()
|
||||
|
||||
if headref is None:
|
||||
return (None, None)
|
||||
|
||||
headref = headref[len("ref: "):]
|
||||
branch = headref[headref.rfind("/") + 1:]
|
||||
with open(os.path.join(gitPath, headref)) as f:
|
||||
head = f.readline().strip()
|
||||
|
||||
return (branch, head)
|
||||
|
||||
|
||||
def getNewTimeout(type):
|
||||
now = time.time()
|
||||
|
||||
if type not in default_settings["serial"]["timeout"].keys():
|
||||
# timeout immediately for unknown timeout type
|
||||
return now
|
||||
|
||||
return now + settings().getFloat(["serial", "timeout", type])
|
||||
|
||||
|
||||
def getFreeBytes(path):
|
||||
def get_free_bytes(path):
|
||||
"""
|
||||
Retrieves the number of free bytes on the partition ``path`` is located at and returns it. Works on both Windows and
|
||||
*nix.
|
||||
|
||||
Taken from http://stackoverflow.com/a/2372171/2028598
|
||||
|
||||
Arguments:
|
||||
path (string): The path for which to check the remaining partition space.
|
||||
|
||||
Returns:
|
||||
int: The amount of bytes still left on the partition.
|
||||
"""
|
||||
|
||||
path = os.path.abspath(path)
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
freeBytes = ctypes.c_ulonglong(0)
|
||||
|
|
@ -114,41 +217,115 @@ def getFreeBytes(path):
|
|||
return st.f_bavail * st.f_frsize
|
||||
|
||||
|
||||
def getRemoteAddress(request):
|
||||
forwardedFor = request.headers.get("X-Forwarded-For", None)
|
||||
if forwardedFor is not None:
|
||||
return forwardedFor.split(",")[0]
|
||||
return request.remote_addr
|
||||
def get_dos_filename(origin, existing_filenames=None, extension=None, **kwargs):
|
||||
"""
|
||||
Converts the provided input filename to a 8.3 DOS compatible filename. If ``existing_filenames`` is provided, the
|
||||
conversion result will be guaranteed not to collide with any of the filenames provided thus.
|
||||
|
||||
Uses :func:`find_collision_free_name` internally.
|
||||
|
||||
def getDosFilename(input, existingFilenames, extension=None):
|
||||
if input is None:
|
||||
Arguments:
|
||||
input (string): The original filename incl. extension to convert to the 8.3 format.
|
||||
existing_filenames (list): A list of existing filenames with which the generated 8.3 name must not collide.
|
||||
Optional.
|
||||
extension (string): The .3 file extension to use for the generated filename. If not provided, the extension of
|
||||
the provided ``filename`` will simply be truncated to 3 characters.
|
||||
kwargs (dict): Additional keyword arguments to provide to :func:`find_collision_free_name`.
|
||||
|
||||
Returns:
|
||||
string: A 8.3 compatible translation of the original filename, not colliding with the optionally provided
|
||||
``existing_filenames`` and with the provided ``extension`` or the original extension shortened to
|
||||
a maximum of 3 characters.
|
||||
|
||||
Raises:
|
||||
ValueError: No 8.3 compatible name could be found that doesn't collide with the provided ``existing_filenames``.
|
||||
"""
|
||||
|
||||
if origin is None:
|
||||
return None
|
||||
|
||||
if existing_filenames is None:
|
||||
existing_filenames = []
|
||||
|
||||
filename, ext = os.path.splitext(origin)
|
||||
if extension is None:
|
||||
extension = "gco"
|
||||
extension = ext
|
||||
|
||||
filename, ext = input.rsplit(".", 1)
|
||||
return findCollisionfreeName(filename, extension, existingFilenames)
|
||||
return find_collision_free_name(filename, extension, existing_filenames, **kwargs)
|
||||
|
||||
|
||||
def findCollisionfreeName(input, extension, existingFilenames):
|
||||
filename = re.sub(r"\s+", "_", input.lower().translate({ord(i):None for i in ".\"/\\[]:;=,"}))
|
||||
def find_collision_free_name(filename, extension, existing_filenames, max_power=2):
|
||||
"""
|
||||
Tries to find a collision free translation of "<filename>.<extension>" to the 8.3 DOS compatible format,
|
||||
preventing collisions with any of the ``existing_filenames``.
|
||||
|
||||
First strips all of ``."/\\[]:;=,`` from the filename and extensions, converts them to lower case and truncates
|
||||
the ``extension`` to a maximum length of 3 characters.
|
||||
|
||||
If the filename is already equal or less than 8 characters in length after that procedure and "<filename>.<extension>"
|
||||
are not contained in the ``existing_files``, that concatenation will be returned as the result.
|
||||
|
||||
If not, the following algorithm will be applied to try to find a collision free name::
|
||||
|
||||
set counter := power := 1
|
||||
while counter < 10^max_power:
|
||||
set truncated := substr(filename, 0, 6 - power + 1) + "~" + counter
|
||||
set result := "<truncated>.<extension>"
|
||||
if result is collision free:
|
||||
return result
|
||||
counter++
|
||||
if counter >= 10 ** power:
|
||||
power++
|
||||
raise ValueError
|
||||
|
||||
This will basically -- for a given original filename of ``some_filename`` and an extension of ``gco`` -- iterate
|
||||
through names of the format ``some_f~1.gco``, ``some_f~2.gco``, ..., ``some_~10.gco``, ``some_~11.gco``, ...,
|
||||
``<prefix>~<n>.gco`` for ``n`` less than 10 ^ ``max_power``, returning as soon as one is found that is not colliding.
|
||||
|
||||
Arguments:
|
||||
filename (string): The filename without the extension to convert to 8.3.
|
||||
extension (string): The extension to convert to 8.3 -- will be truncated to 3 characters if it's longer than
|
||||
that.
|
||||
existing_filenames (list): A list of existing filenames to prevent name collisions with.
|
||||
max_power (int): Limits the possible attempts of generating a collision free name to 10 ^ ``max_power``
|
||||
variations. Defaults to 2, so the name generation will maximally reach ``<name>~99.<ext>`` before
|
||||
aborting and raising an exception.
|
||||
|
||||
Returns:
|
||||
string: A 8.3 representation of the provided original filename, ensured to not collide with the provided
|
||||
``existing_filenames``
|
||||
|
||||
Raises:
|
||||
ValueError: No collision free name could be found.
|
||||
"""
|
||||
|
||||
# TODO unit test!
|
||||
|
||||
def make_valid(text):
|
||||
return re.sub(r"\s+", "_", text.translate({ord(i):None for i in ".\"/\\[]:;=,"})).lower()
|
||||
|
||||
filename = make_valid(filename)
|
||||
extension = make_valid(extension)
|
||||
extension = extension[:3] if len(extension) > 3 else extension
|
||||
|
||||
if len(filename) <= 8 and not filename + "." + extension in existing_filenames:
|
||||
# early exit
|
||||
return filename + "." + extension
|
||||
|
||||
counter = 1
|
||||
power = 1
|
||||
while counter < (10 * power):
|
||||
while counter < (10 ** max_power):
|
||||
result = filename[:(6 - power + 1)] + "~" + str(counter) + "." + extension
|
||||
if result not in existingFilenames:
|
||||
if result not in existing_filenames:
|
||||
return result
|
||||
counter += 1
|
||||
if counter == 10 * power:
|
||||
if counter >= 10 ** power:
|
||||
power += 1
|
||||
|
||||
raise ValueError("Can't create a collision free filename")
|
||||
|
||||
|
||||
def safeRename(old, new, throw_error=False):
|
||||
def safe_rename(old, new, throw_error=False):
|
||||
"""
|
||||
Safely renames a file.
|
||||
|
||||
|
|
@ -157,10 +334,16 @@ def safeRename(old, new, throw_error=False):
|
|||
anything goes wrong during those steps, the backup (if already there) will be renamed to its old name and thus
|
||||
the operation hopefully result in a no-op.
|
||||
|
||||
On other operating systems the atomic os.rename function will be used instead.
|
||||
On other operating systems :func:`shutil.move` will be used instead.
|
||||
|
||||
@param old the path to the old file to be renamed
|
||||
@param new the path to the new file to be created/replaced
|
||||
Arguments:
|
||||
old (string): The path to the old file to be renamed.
|
||||
new (string): The path to the new file to be created/replaced.
|
||||
throw_error (boolean): Whether to throw an error upon errors during the renaming procedure (True) or not
|
||||
(False, default).
|
||||
|
||||
Raises:
|
||||
OSError: One of the renaming steps on windows failed and ``throw_error`` was True
|
||||
"""
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
|
@ -169,7 +352,7 @@ def safeRename(old, new, throw_error=False):
|
|||
|
||||
try:
|
||||
if os.path.exists(new):
|
||||
silentRemove(backup)
|
||||
silent_remove(backup)
|
||||
os.rename(new, backup)
|
||||
os.rename(old, new)
|
||||
os.remove(backup)
|
||||
|
|
@ -177,7 +360,7 @@ def safeRename(old, new, throw_error=False):
|
|||
# if anything went wrong, try to rename the backup file to its original name
|
||||
logger.error("Could not perform safe rename, trying to revert")
|
||||
if os.path.exists(backup):
|
||||
silentRemove(new)
|
||||
silent_remove(new)
|
||||
os.rename(backup, new)
|
||||
if throw_error:
|
||||
raise e
|
||||
|
|
@ -186,11 +369,12 @@ def safeRename(old, new, throw_error=False):
|
|||
shutil.move(old, new)
|
||||
|
||||
|
||||
def silentRemove(file):
|
||||
def silent_remove(file):
|
||||
"""
|
||||
Silently removes a file. Does not raise an error if the file doesn't exist.
|
||||
|
||||
@param file the path of the file to be removed
|
||||
Arguments:
|
||||
file (string): The path of the file to be removed
|
||||
"""
|
||||
|
||||
try:
|
||||
|
|
@ -199,15 +383,19 @@ def silentRemove(file):
|
|||
pass
|
||||
|
||||
|
||||
def sanitizeAscii(line):
|
||||
def sanitize_ascii(line):
|
||||
return unicode(line, 'ascii', 'replace').encode('ascii', 'replace').rstrip()
|
||||
|
||||
|
||||
def filterNonAscii(line):
|
||||
def filter_non_ascii(line):
|
||||
"""
|
||||
Returns True if the line contains non-ascii characters, false otherwise
|
||||
Filter predicate to test if a line contains non ASCII characters.
|
||||
|
||||
@param line the line to test
|
||||
Arguments:
|
||||
line (string): The line to test
|
||||
|
||||
Returns:
|
||||
boolean: True if the line contains non ASCII characters, False otherwise.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
|
@ -217,28 +405,19 @@ def filterNonAscii(line):
|
|||
return True
|
||||
|
||||
|
||||
def getJsonCommandFromRequest(request, valid_commands):
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return None, None, make_response("Expected content-type JSON", 400)
|
||||
|
||||
data = request.json
|
||||
if not "command" in data.keys() or not data["command"] in valid_commands.keys():
|
||||
return None, None, make_response("Expected valid command", 400)
|
||||
|
||||
command = data["command"]
|
||||
for parameter in valid_commands[command]:
|
||||
if not parameter in data:
|
||||
return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400)
|
||||
|
||||
return command, data, None
|
||||
|
||||
|
||||
def dict_merge(a, b):
|
||||
'''recursively merges dict's. not just simple a['key'] = b['key'], if
|
||||
both a and bhave a key who's value is a dict then dict_merge is called
|
||||
on both values and the result stored in the returned dictionary.
|
||||
"""
|
||||
Recursively deep-merges two dictionaries.
|
||||
|
||||
Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/'''
|
||||
Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/
|
||||
|
||||
Arguments:
|
||||
a (dict): The dictionary to merge ``b`` into
|
||||
b (dict): The dictionary to merge into ``a``
|
||||
|
||||
Returns:
|
||||
dict: ``b`` deep-merged into ``a``
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
|
|
@ -254,6 +433,17 @@ def dict_merge(a, b):
|
|||
|
||||
|
||||
def dict_clean(a, b):
|
||||
"""
|
||||
Recursively deep-cleans ``b`` from ``a``, removing all keys and corresponding values from ``a`` that appear in
|
||||
``b``.
|
||||
|
||||
Arguments:
|
||||
a (dict): The dictionary to clean from ``b``.
|
||||
b (dict): The dictionary to clean ``b`` from.
|
||||
|
||||
Results:
|
||||
dict: A new dict based on ``a`` with all keys (and corresponding values) found in ``b`` removed.
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
if not isinstance(b, dict):
|
||||
|
|
@ -271,6 +461,24 @@ def dict_clean(a, b):
|
|||
|
||||
|
||||
def dict_contains_keys(a, b):
|
||||
"""
|
||||
Recursively deep-checks if ``a`` contains all keys found in ``b``.
|
||||
|
||||
Example::
|
||||
|
||||
>>> dict_contains_keys(dict(foo="bar", fnord=dict(a=1, b=2, c=3)), dict(foo="some_other_bar", fnord=dict(b=100)))
|
||||
True
|
||||
>>> dict_contains_keys(dict(foo="bar", fnord=dict(a=1, b=2, c=3)), dict(foo="some_other_bar", fnord=dict(b=100, d=20)))
|
||||
False
|
||||
|
||||
Arguments:
|
||||
a (dict): The dictionary to check for the keys from ``b``.
|
||||
b (dict): The dictionary whose keys to check ``a`` for.
|
||||
|
||||
Returns:
|
||||
boolean: True if all keys found in ``b`` are also present in ``a``, False otherwise.
|
||||
"""
|
||||
|
||||
if not isinstance(a, dict) or not isinstance(b, dict):
|
||||
return False
|
||||
|
||||
|
|
@ -283,11 +491,14 @@ def dict_contains_keys(a, b):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
class Object(object):
|
||||
pass
|
||||
|
||||
def interface_addresses(family=None):
|
||||
"""
|
||||
Retrieves all of the host's network interface addresses.
|
||||
"""
|
||||
|
||||
import netifaces
|
||||
if not family:
|
||||
family = netifaces.AF_INET
|
||||
|
|
@ -303,6 +514,10 @@ def interface_addresses(family=None):
|
|||
yield ifaddress["addr"]
|
||||
|
||||
def address_for_client(host, port):
|
||||
"""
|
||||
Determines the address of the network interface on this host needed to connect to the indicated client host and port.
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
for address in interface_addresses():
|
||||
|
|
@ -311,7 +526,7 @@ def address_for_client(host, port):
|
|||
sock.bind((address, 0))
|
||||
sock.connect((host, port))
|
||||
return address
|
||||
except Exception as e:
|
||||
pass
|
||||
except:
|
||||
continue
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ from collections import deque
|
|||
from octoprint.util.avr_isp import stk500v2
|
||||
from octoprint.util.avr_isp import ispBase
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.settings import settings, default_settings
|
||||
from octoprint.events import eventManager, Events
|
||||
from octoprint.filemanager import valid_file_type
|
||||
from octoprint.filemanager.destinations import FileDestinations
|
||||
from octoprint.util import getExceptionString, getNewTimeout, sanitizeAscii, filterNonAscii
|
||||
from octoprint.util import get_exception_string, sanitize_ascii, filter_non_ascii
|
||||
from octoprint.util.virtual import VirtualPrinter
|
||||
|
||||
try:
|
||||
|
|
@ -172,6 +172,7 @@ class MachineCom(object):
|
|||
self._sdAvailable = False
|
||||
self._sdFileList = False
|
||||
self._sdFiles = []
|
||||
self._sdFileToSelect = None
|
||||
|
||||
# print job
|
||||
self._currentFile = None
|
||||
|
|
@ -479,7 +480,7 @@ class MachineCom(object):
|
|||
self._sendFromQueue(sendChecksum=True)
|
||||
except:
|
||||
self._logger.exception("Error while trying to start printing")
|
||||
self._errorValue = getExceptionString()
|
||||
self._errorValue = get_exception_string()
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
||||
|
||||
|
|
@ -503,6 +504,7 @@ class MachineCom(object):
|
|||
if not self.isOperational():
|
||||
# printer is not connected, can't use SD
|
||||
return
|
||||
self._sdFileToSelect = filename
|
||||
self.sendCommand("M23 %s" % filename)
|
||||
else:
|
||||
self._currentFile = PrintingGcodeFileInformation(filename, self.getOffsets)
|
||||
|
|
@ -728,10 +730,10 @@ class MachineCom(object):
|
|||
self._changeState(self.STATE_CONNECTING)
|
||||
|
||||
#Start monitoring the serial port.
|
||||
self._timeout = getNewTimeout("communication")
|
||||
self._timeout = get_new_timeout("communication")
|
||||
|
||||
tempRequestTimeout = getNewTimeout("temperature")
|
||||
sdStatusRequestTimeout = getNewTimeout("sdStatus")
|
||||
tempRequestTimeout = get_new_timeout("temperature")
|
||||
sdStatusRequestTimeout = get_new_timeout("sdStatus")
|
||||
|
||||
startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"])
|
||||
swallowOk = False
|
||||
|
|
@ -743,7 +745,7 @@ class MachineCom(object):
|
|||
if line is None:
|
||||
break
|
||||
if line.strip() is not "":
|
||||
self._timeout = getNewTimeout("communication")
|
||||
self._timeout = get_new_timeout("communication")
|
||||
|
||||
##~~ debugging output handling
|
||||
if line.startswith("//"):
|
||||
|
|
@ -789,9 +791,12 @@ class MachineCom(object):
|
|||
size = None
|
||||
|
||||
if valid_file_type(filename, "gcode"):
|
||||
if filterNonAscii(filename):
|
||||
if filter_non_ascii(filename):
|
||||
self._logger.warn("Got a file from printer's SD that has a non-ascii filename (%s), that shouldn't happen according to the protocol" % filename)
|
||||
else:
|
||||
if not filename.startswith("/"):
|
||||
# file from the root of the sd -- we'll prepend a /
|
||||
filename = "/" + filename
|
||||
self._sdFiles.append((filename, size))
|
||||
continue
|
||||
|
||||
|
|
@ -861,7 +866,12 @@ class MachineCom(object):
|
|||
elif 'File opened' in line:
|
||||
# answer to M23, at least on Marlin, Repetier and Sprinter: "File opened:%s Size:%d"
|
||||
match = self._regex_sdFileOpened.search(line)
|
||||
self._currentFile = PrintingSdFileInformation(match.group(1), int(match.group(2)))
|
||||
if self._sdFileToSelect:
|
||||
name = self._sdFileToSelect
|
||||
self._sdFileToSelect = None
|
||||
else:
|
||||
name = match.group(1)
|
||||
self._currentFile = PrintingSdFileInformation(name, int(match.group(2)))
|
||||
elif 'File selected' in line:
|
||||
# final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected"
|
||||
if self._currentFile is not None:
|
||||
|
|
@ -950,12 +960,12 @@ class MachineCom(object):
|
|||
self._log("Trying baudrate: %d" % (baudrate))
|
||||
self._baudrateDetectRetry = 5
|
||||
self._baudrateDetectTestOk = 0
|
||||
self._timeout = getNewTimeout("communication")
|
||||
self._timeout = get_new_timeout("communication")
|
||||
self._serial.write('\n')
|
||||
self._sendCommand("M105")
|
||||
self._testingBaudrate = True
|
||||
except:
|
||||
self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
|
||||
self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, get_exception_string()))
|
||||
elif 'ok' in line and 'T:' in line:
|
||||
self._baudrateDetectTestOk += 1
|
||||
if self._baudrateDetectTestOk < 10:
|
||||
|
|
@ -984,7 +994,7 @@ class MachineCom(object):
|
|||
#Request the temperature on comm timeout (every 5 seconds) when we are not printing.
|
||||
if line == "":
|
||||
self.sendCommand("M105")
|
||||
tempRequestTimeout = getNewTimeout("temperature")
|
||||
tempRequestTimeout = get_new_timeout("temperature")
|
||||
|
||||
# if we still have commands to process, process them
|
||||
elif "ok" in line:
|
||||
|
|
@ -1018,11 +1028,11 @@ class MachineCom(object):
|
|||
if self._heatupWaitStartTime is None:
|
||||
if time.time() > tempRequestTimeout:
|
||||
self.sendCommand("M105")
|
||||
tempRequestTimeout = getNewTimeout("temperature")
|
||||
tempRequestTimeout = get_new_timeout("temperature")
|
||||
|
||||
if self.isSdPrinting() and time.time() > sdStatusRequestTimeout:
|
||||
self.sendCommand("M27")
|
||||
sdStatusRequestTimeout = getNewTimeout("sdStatus")
|
||||
sdStatusRequestTimeout = get_new_timeout("sdStatus")
|
||||
|
||||
if self._sendFromQueue(sendChecksum=True):
|
||||
pass
|
||||
|
|
@ -1074,7 +1084,7 @@ class MachineCom(object):
|
|||
self._log("Error while connecting to %s: %s" % (p, str(e)))
|
||||
pass
|
||||
except:
|
||||
self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
|
||||
self._log("Unexpected error while connecting to serial port: %s %s" % (p, get_exception_string()))
|
||||
programmer.close()
|
||||
if self._serial is None:
|
||||
self._log("Failed to autodetect serial port")
|
||||
|
|
@ -1097,7 +1107,7 @@ class MachineCom(object):
|
|||
self._serial.parity = serial.PARITY_NONE
|
||||
self._serial.open()
|
||||
except:
|
||||
self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
|
||||
self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, get_exception_string()))
|
||||
self._errorValue = "Failed to open serial port, permissions correct?"
|
||||
self._changeState(self.STATE_ERROR)
|
||||
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
||||
|
|
@ -1136,14 +1146,14 @@ class MachineCom(object):
|
|||
try:
|
||||
ret = self._serial.readline()
|
||||
except:
|
||||
self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
|
||||
self._errorValue = getExceptionString()
|
||||
self._log("Unexpected error while reading serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
self.close(True)
|
||||
return None
|
||||
if ret == '':
|
||||
#self._log("Recv: TIMEOUT")
|
||||
return ''
|
||||
self._log("Recv: %s" % sanitizeAscii(ret))
|
||||
self._log("Recv: %s" % sanitize_ascii(ret))
|
||||
return ret
|
||||
|
||||
def _getNext(self):
|
||||
|
|
@ -1293,12 +1303,12 @@ class MachineCom(object):
|
|||
try:
|
||||
self._serial.write(cmd + '\n')
|
||||
except:
|
||||
self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
|
||||
self._errorValue = getExceptionString()
|
||||
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
self.close(True)
|
||||
except:
|
||||
self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
|
||||
self._errorValue = getExceptionString()
|
||||
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
|
||||
self._errorValue = get_exception_string()
|
||||
self.close(True)
|
||||
|
||||
def _gcode_T(self, cmd):
|
||||
|
|
@ -1403,7 +1413,7 @@ class MachineCom(object):
|
|||
elif s_idx != -1:
|
||||
# dwell time is specified in seconds
|
||||
_timeout = int(cmd[s_idx+1:])
|
||||
self._timeout = getNewTimeout("communication") + _timeout
|
||||
self._timeout = get_new_timeout("communication") + _timeout
|
||||
return cmd
|
||||
|
||||
### MachineCom callback ################################################################################################
|
||||
|
|
@ -1642,3 +1652,13 @@ class StreamingGcodeFileInformation(PrintingGcodeFileInformation):
|
|||
|
||||
def getRemoteFilename(self):
|
||||
return self._remoteFilename
|
||||
|
||||
|
||||
def get_new_timeout(type):
|
||||
now = time.time()
|
||||
|
||||
if type not in default_settings["serial"]["timeout"].keys():
|
||||
# timeout immediately for unknown timeout type
|
||||
return now
|
||||
|
||||
return now + settings().getFloat(["serial", "timeout", type])
|
||||
258
tests/plugin/test_settings.py
Normal file
258
tests/plugin/test_settings.py
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import unittest
|
||||
import mock
|
||||
import warnings
|
||||
|
||||
from ddt import ddt, unpack, data
|
||||
|
||||
import octoprint.plugin
|
||||
import octoprint.settings
|
||||
|
||||
@ddt
|
||||
class SettingsTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
warnings.simplefilter("always")
|
||||
|
||||
self.plugin_key = "test_plugin"
|
||||
|
||||
self.settings = mock.create_autospec(octoprint.settings.Settings)
|
||||
|
||||
self.defaults = dict(
|
||||
some_raw_key="some_raw_value",
|
||||
some_int_key=1,
|
||||
some_float_key=2.5,
|
||||
some_boolean_key=True,
|
||||
)
|
||||
|
||||
self.plugin_settings = octoprint.plugin.PluginSettings(self.settings, self.plugin_key, defaults=self.defaults)
|
||||
|
||||
@data(
|
||||
("get", (["some_raw_key",],), dict(), "get"),
|
||||
("get", (["some_raw_key",],), dict(merged=True), "get"),
|
||||
("get", (["some_raw_key",],), dict(asdict=True), "get"),
|
||||
("get", (["some_raw_key",],), dict(merged=True, asdict=True), "get"),
|
||||
("get_int", (["some_int_key,"],), dict(), "getInt"),
|
||||
("get_float", (["some_float_key"],), dict(), "getFloat"),
|
||||
("get_boolean", (["some_boolean_key",],), dict(), "getBoolean"),
|
||||
)
|
||||
@unpack
|
||||
def test_forwarded_getter(self, getter, getter_args, getter_kwargs, forwarded):
|
||||
method_under_test = getattr(self.plugin_settings, getter)
|
||||
self.assertTrue(callable(method_under_test))
|
||||
|
||||
method_under_test(*getter_args, **getter_kwargs)
|
||||
|
||||
forwarded_method = getattr(self.settings, forwarded)
|
||||
forwarded_args = (["plugins", self.plugin_key] + getter_args[0],)
|
||||
forwarded_kwargs = getter_kwargs
|
||||
forwarded_kwargs.update(dict(defaults=dict(plugins=dict(test_plugin=self.defaults))))
|
||||
forwarded_method.assert_called_once_with(*forwarded_args, **forwarded_kwargs)
|
||||
|
||||
@data(
|
||||
("global_get", (["some_raw_key"],), dict(), "get"),
|
||||
("global_get", (["some_raw_key"],), dict(merged=True), "get"),
|
||||
("global_get", (["some_raw_key"],), dict(asdict=True), "get"),
|
||||
("global_get", (["some_raw_key"],), dict(merged=True, asdict=True), "get"),
|
||||
("global_get_int", (["some_int_key"],), dict(), "getInt"),
|
||||
("global_get_float", (["some_float_key"],), dict(), "getFloat"),
|
||||
("global_get_boolean", (["some_boolean_key"],), dict(), "getBoolean")
|
||||
)
|
||||
@unpack
|
||||
def test_global_getter(self, getter, getter_args, getter_kwargs, forwarded):
|
||||
method_under_test = getattr(self.plugin_settings, getter)
|
||||
self.assertTrue(callable(method_under_test))
|
||||
|
||||
method_under_test(*getter_args, **getter_kwargs)
|
||||
|
||||
forwarded_method = getattr(self.settings, forwarded)
|
||||
forwarded_method.assert_called_once_with(*getter_args, **getter_kwargs)
|
||||
|
||||
@data(
|
||||
("getInt", "get_int", "getInt"),
|
||||
("getFloat", "get_float", "getFloat"),
|
||||
("getBoolean", "get_boolean", "getBoolean"),
|
||||
)
|
||||
@unpack
|
||||
def test_deprecated_forwarded_getter(self, deprecated, current, forwarded):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
called_method = getattr(self.settings, forwarded)
|
||||
called_method.__name__ = forwarded
|
||||
|
||||
method = getattr(self.plugin_settings, deprecated)
|
||||
self.assertTrue(callable(method))
|
||||
method(["some_raw_key"])
|
||||
|
||||
called_method.assert_called_once_with(["plugins", self.plugin_key, "some_raw_key"], defaults=dict(plugins=dict(test_plugin=self.defaults)))
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
|
||||
|
||||
@data(
|
||||
("globalGet", "global_get", "get"),
|
||||
("globalGetInt", "global_get_int", "getInt"),
|
||||
("globalGetFloat", "global_get_float", "getFloat"),
|
||||
("globalGetBoolean", "global_get_boolean", "getBoolean")
|
||||
)
|
||||
@unpack
|
||||
def test_deprecated_global_getter(self, deprecated, current, forwarded):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
called_method = getattr(self.settings, forwarded)
|
||||
called_method.__name__ = forwarded
|
||||
|
||||
method = getattr(self.plugin_settings, deprecated)
|
||||
self.assertTrue(callable(method))
|
||||
method(["some_raw_key"])
|
||||
|
||||
called_method.assert_called_once_with(["some_raw_key",])
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
|
||||
|
||||
@data(
|
||||
("set", (["some_raw_key",], "some_value"), dict(), "set"),
|
||||
("set", (["some_raw_key",], "some_value"), dict(force=True), "set"),
|
||||
("set_int", (["some_int_key",], 23), dict(), "setInt"),
|
||||
("set_int", (["some_int_key",], 23), dict(force=True), "setInt"),
|
||||
("set_float", (["some_float_key",], 2.3), dict(), "setFloat"),
|
||||
("set_float", (["some_float_key",], 2.3), dict(force=True), "setFloat"),
|
||||
("set_boolean", (["some_boolean_key",], True), dict(), "setBoolean"),
|
||||
("set_boolean", (["some_boolean_key",], True), dict(force=True), "setBoolean"),
|
||||
)
|
||||
@unpack
|
||||
def test_forwarded_setter(self, setter, setter_args, setter_kwargs, forwarded):
|
||||
method_under_test = getattr(self.plugin_settings, setter)
|
||||
self.assertTrue(callable(method_under_test))
|
||||
|
||||
method_under_test(*setter_args, **setter_kwargs)
|
||||
|
||||
forwarded_method = getattr(self.settings, forwarded)
|
||||
forwarded_args = (["plugins", self.plugin_key] + setter_args[0], setter_args[1])
|
||||
forwarded_kwargs = setter_kwargs
|
||||
forwarded_kwargs.update(dict(defaults=dict(plugins=dict(test_plugin=self.defaults))))
|
||||
forwarded_method.assert_called_once_with(*forwarded_args, **forwarded_kwargs)
|
||||
|
||||
@data(
|
||||
("global_set", (["some_raw_key",], "some_value"), dict(), "set"),
|
||||
("global_set", (["some_raw_key",], "some_value"), dict(force=True), "set"),
|
||||
("global_set_int", (["some_int_key",], 23), dict(), "setInt"),
|
||||
("global_set_int", (["some_int_key",], 23), dict(force=True), "setInt"),
|
||||
("global_set_float", (["some_float_key",], 2.3), dict(), "setFloat"),
|
||||
("global_set_float", (["some_float_key",], 2.3), dict(force=True), "setFloat"),
|
||||
("global_set_boolean", (["some_boolean_key",], True), dict(), "setBoolean"),
|
||||
("global_set_boolean", (["some_boolean_key",], True), dict(force=True), "setBoolean"),
|
||||
)
|
||||
@unpack
|
||||
def test_global_setter(self, setter, setter_args, setter_kwargs, forwarded):
|
||||
method_under_test = getattr(self.plugin_settings, setter)
|
||||
self.assertTrue(callable(method_under_test))
|
||||
|
||||
method_under_test(*setter_args, **setter_kwargs)
|
||||
|
||||
forwarded_method = getattr(self.settings, forwarded)
|
||||
forwarded_method.assert_called_once_with(*setter_args, **setter_kwargs)
|
||||
|
||||
@data(
|
||||
("setInt", "set_int", "setInt", 1),
|
||||
("setFloat", "set_float", "setFloat", 2.5),
|
||||
("setBoolean", "set_boolean", "setBoolean", True)
|
||||
)
|
||||
@unpack
|
||||
def test_deprecated_forwarded_setter(self, deprecated, current, forwarded, value):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
called_method = getattr(self.settings, forwarded)
|
||||
called_method.__name__ = forwarded
|
||||
|
||||
method = getattr(self.plugin_settings, deprecated)
|
||||
self.assertTrue(callable(method))
|
||||
method(["some_raw_key"], value)
|
||||
|
||||
called_method.assert_called_once_with(["plugins", self.plugin_key, "some_raw_key"], value, defaults=dict(plugins=dict(test_plugin=self.defaults)))
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
|
||||
|
||||
@data(
|
||||
("globalSet", "global_set", "set", "some_value"),
|
||||
("globalSetInt", "global_set_int", "setInt", 1),
|
||||
("globalSetFloat", "global_set_float", "setFloat", 2.5),
|
||||
("globalSetBoolean", "global_set_boolean", "setBoolean", True)
|
||||
)
|
||||
@unpack
|
||||
def test_deprecated_global_setter(self, deprecated, current, forwarded, value):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
called_method = getattr(self.settings, forwarded)
|
||||
called_method.__name__ = forwarded
|
||||
|
||||
method = getattr(self.plugin_settings, deprecated)
|
||||
self.assertTrue(callable(method))
|
||||
method(["some_raw_key"], value)
|
||||
|
||||
called_method.assert_called_once_with(["some_raw_key"], value)
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("{old} has been renamed to {new}".format(old=deprecated, new=current) in (w[-1].message))
|
||||
|
||||
def test_global_get_basefolder(self):
|
||||
self.plugin_settings.global_get_basefolder("some_folder")
|
||||
self.settings.getBaseFolder.assert_called_once_with("some_folder")
|
||||
|
||||
def test_deprecated_global_get_basefolder(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
self.plugin_settings.globalGetBaseFolder("some_folder")
|
||||
self.settings.getBaseFolder.assert_called_once_with("some_folder")
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("globalGetBaseFolder has been renamed to global_get_basefolder" in (w[-1].message))
|
||||
|
||||
def test_logfile_path(self):
|
||||
import os
|
||||
|
||||
self.settings.getBaseFolder.return_value = "/some/folder"
|
||||
|
||||
path = self.plugin_settings.get_plugin_logfile_path()
|
||||
|
||||
self.settings.getBaseFolder.assert_called_once_with("logs")
|
||||
self.assertEquals("/some/folder/plugin_{key}.log".format(key=self.plugin_key), path.replace(os.sep, "/"))
|
||||
|
||||
def test_logfile_path_with_postfix(self):
|
||||
import os
|
||||
|
||||
self.settings.getBaseFolder.return_value = "/some/folder"
|
||||
|
||||
path = self.plugin_settings.get_plugin_logfile_path(postfix="mypostfix")
|
||||
|
||||
self.settings.getBaseFolder.assert_called_once_with("logs")
|
||||
self.assertEquals("/some/folder/plugin_{key}_mypostfix.log".format(key=self.plugin_key), path.replace(os.sep, "/"))
|
||||
|
||||
def test_deprecated_logfile_path(self):
|
||||
import os
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
self.settings.getBaseFolder.return_value = "/some/folder"
|
||||
path = self.plugin_settings.getPluginLogfilePath()
|
||||
|
||||
self.settings.getBaseFolder.assert_called_once_with("logs")
|
||||
self.assertEquals("/some/folder/plugin_{key}.log".format(key=self.plugin_key), path.replace(os.sep, "/"))
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertTrue("getPluginLogfilePath has been renamed to get_plugin_logfile_path" in (w[-1].message))
|
||||
|
||||
def test_unhandled_method(self):
|
||||
try:
|
||||
self.plugin_settings.some_method("some_parameter")
|
||||
except AttributeError as e:
|
||||
self.assertTrue("Mock object has no attribute 'some_method'" in str(e))
|
||||
Loading…
Reference in a new issue