More cleanup for the feedback controls
Preprocessing for better performancy, unit tests for preprocessing, controls don't need a type anymore (makes things way less complicated and repetitive)
This commit is contained in:
parent
7ed9cc0f87
commit
22c73d831a
13 changed files with 421 additions and 195 deletions
|
|
@ -69,6 +69,7 @@ class Events(object):
|
|||
CONVEYOR = "Conveyor"
|
||||
EJECT = "Eject"
|
||||
E_STOP = "EStop"
|
||||
REGISTERED_MESSAGE_RECEIVED = "RegisteredMessageReceived"
|
||||
|
||||
# Timelapse
|
||||
CAPTURE_START = "CaptureStart"
|
||||
|
|
|
|||
|
|
@ -97,19 +97,21 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
|
|||
return _instance
|
||||
|
||||
|
||||
def plugin_settings(plugin_key, defaults=None):
|
||||
def plugin_settings(plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=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.
|
||||
get_preprocessors (dict): The getter preprocessors for the plugin.
|
||||
set_preprocessors (dict): The setter preprocessors 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)
|
||||
return PluginSettings(settings(), plugin_key, defaults=defaults, get_preprocessors=get_preprocessors, set_preprocessors=set_preprocessors)
|
||||
|
||||
|
||||
def call_plugin(types, method, args=None, kwargs=None, callback=None, error_callback=None):
|
||||
|
|
@ -234,7 +236,7 @@ class PluginSettings(object):
|
|||
Like :func:`set` but ensures the value is an ``boolean`` through attempted conversion before setting it.
|
||||
"""
|
||||
|
||||
def __init__(self, settings, plugin_key, defaults=None):
|
||||
def __init__(self, settings, plugin_key, defaults=None, get_preprocessors=None, set_preprocessors=None):
|
||||
self.settings = settings
|
||||
self.plugin_key = plugin_key
|
||||
|
||||
|
|
@ -243,6 +245,16 @@ class PluginSettings(object):
|
|||
self.defaults = dict(plugins=dict())
|
||||
self.defaults["plugins"][plugin_key] = defaults
|
||||
|
||||
if get_preprocessors is None:
|
||||
get_preprocessors = dict()
|
||||
self.get_preprocessors = dict(plugins=dict())
|
||||
self.get_preprocessors["plugins"][plugin_key] = get_preprocessors
|
||||
|
||||
if set_preprocessors is None:
|
||||
set_preprocessors = dict()
|
||||
self.set_preprocessors = dict(plugins=dict())
|
||||
self.set_preprocessors["plugins"][plugin_key] = set_preprocessors
|
||||
|
||||
def prefix_path(path):
|
||||
return ['plugins', self.plugin_key] + path
|
||||
|
||||
|
|
@ -259,21 +271,32 @@ class PluginSettings(object):
|
|||
result.extend(args_after)
|
||||
return result
|
||||
|
||||
def add_defaults_to_kwargs(kwargs):
|
||||
kwargs.update(dict(defaults=self.defaults))
|
||||
def add_getter_kwargs(kwargs):
|
||||
kwargs.update(defaults=self.defaults, preprocessors=self.get_preprocessors)
|
||||
return kwargs
|
||||
|
||||
def add_setter_kwargs(kwargs):
|
||||
kwargs.update(defaults=self.defaults, preprocessors=self.set_preprocessors)
|
||||
return 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))
|
||||
get =("get", prefix_path_in_args, add_getter_kwargs),
|
||||
get_int =("getInt", prefix_path_in_args, add_getter_kwargs),
|
||||
get_float =("getFloat", prefix_path_in_args, add_getter_kwargs),
|
||||
get_boolean=("getBoolean", prefix_path_in_args, add_getter_kwargs),
|
||||
set =("set", prefix_path_in_args, add_setter_kwargs),
|
||||
set_int =("setInt", prefix_path_in_args, add_setter_kwargs),
|
||||
set_float =("setFloat", prefix_path_in_args, add_setter_kwargs),
|
||||
set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs)
|
||||
)
|
||||
self.deprecated_access_methods = dict(
|
||||
getInt ="get_int",
|
||||
getFloat ="get_float",
|
||||
getBoolean="get_boolean",
|
||||
setInt ="set_int",
|
||||
setFloat ="set_float",
|
||||
setBoolean="set_boolean"
|
||||
)
|
||||
self.deprecated_access_methods = dict(getInt="get_int", getFloat="get_float", getBoolean="get_boolean", setInt="set_int", setFloat="set_float", setBoolean="set_boolean")
|
||||
|
||||
def global_get(self, path, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -797,7 +797,8 @@ class SettingsPlugin(OctoPrintPlugin):
|
|||
and iterate yourself over all your settings, retrieving them (if set) from the supplied received ``data``
|
||||
and using the proper setter methods on the settings manager to persist the data in the correct format.
|
||||
|
||||
:param dict data: the settings dictionary to be saved for the plugin
|
||||
Arguments:
|
||||
data (dict): The settings dictionary to be saved for the plugin
|
||||
"""
|
||||
import octoprint.util
|
||||
|
||||
|
|
@ -814,6 +815,43 @@ class SettingsPlugin(OctoPrintPlugin):
|
|||
"""
|
||||
return dict()
|
||||
|
||||
def get_settings_preprocessors(self):
|
||||
"""
|
||||
Retrieves the plugin's preprocessors to use for preprocessing returned or set values prior to returning/setting
|
||||
them.
|
||||
|
||||
The preprocessors should be provided as a dictionary mapping the path of the values to preprocess
|
||||
(hierarchically) to a transform function which will get the value to transform as only input and should return
|
||||
the transformed value.
|
||||
|
||||
Example:
|
||||
|
||||
.. sourcecode: python
|
||||
|
||||
def get_settings_defaults(self):
|
||||
return dict(some_key="Some_Value", some_other_key="Some_Value")
|
||||
|
||||
def get_settings_preprocessors(self):
|
||||
return dict(some_key=lambda x: x.upper()), # getter preprocessors
|
||||
dict(some_other_key=lambda x: x.lower()) # setter preprocessors
|
||||
|
||||
def some_method(self):
|
||||
# getting the value for "some_key" should turn it to upper case
|
||||
assert self._settings.get(["some_key"]) == "SOME_VALUE"
|
||||
|
||||
# the value for "some_other_key" should be left alone
|
||||
assert self._settings.get(["some_other_key"] = "Some_Value"
|
||||
|
||||
# setting a value for "some_other_key" should cause the value to first be turned to lower case
|
||||
self._settings.set(["some_other_key"], "SOME_OTHER_VALUE")
|
||||
assert self._settings.get(["some_other_key"]) == "some_other_value"
|
||||
|
||||
Returns:
|
||||
(dict, dict): A tuple consisting of two dictionaries, the first being the plugin's preprocessors for
|
||||
getters, the second the preprocessors for setters
|
||||
"""
|
||||
return dict(), dict()
|
||||
|
||||
|
||||
class EventHandlerPlugin(OctoPrintPlugin):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -149,11 +149,6 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
try: callback.on_printer_send_current_data(copy.deepcopy(data))
|
||||
except: self._logger.exception("Exception while pushing current data")
|
||||
|
||||
def _sendFeedbackCommandOutput(self, name, output):
|
||||
for callback in self._callbacks:
|
||||
try: callback.on_printer_received_registered_message(name, output)
|
||||
except: self._logger.exception("Exception while pushing feedback command output")
|
||||
|
||||
#~~ callback from metadata analysis event
|
||||
|
||||
def _on_event_MetadataAnalysisFinished(self, event, data):
|
||||
|
|
@ -836,9 +831,6 @@ class Printer(PrinterInterface, comm.MachineComPrintCallback):
|
|||
self._setProgressData(None, None, None, None)
|
||||
self._stateMonitor.set_state({"text": self.get_state_string(), "flags": self._getStateFlags()})
|
||||
|
||||
def on_comm_received_registered_message(self, command, output):
|
||||
self._sendFeedbackCommandOutput(command, output)
|
||||
|
||||
def on_comm_force_disconnect(self):
|
||||
self.disconnect()
|
||||
|
||||
|
|
|
|||
|
|
@ -549,7 +549,11 @@ class Server():
|
|||
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)
|
||||
get_preprocessors, set_preprocessors = implementation.get_settings_preprocessors()
|
||||
plugin_settings = octoprint.plugin.plugin_settings(name,
|
||||
defaults=default_settings,
|
||||
get_preprocessors=get_preprocessors,
|
||||
set_preprocessors=set_preprocessors)
|
||||
return dict(settings=plugin_settings)
|
||||
|
||||
pluginManager.initialize_implementations(
|
||||
|
|
|
|||
|
|
@ -111,9 +111,6 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
|
|||
def sendEvent(self, type, payload=None):
|
||||
self._emit("event", {"type": type, "payload": payload})
|
||||
|
||||
def on_printer_received_registered_message(self, name, output):
|
||||
self._emit("feedbackCommandOutput", {"name": name, "output": output})
|
||||
|
||||
def sendTimelapseConfig(self, timelapseConfig):
|
||||
self._emit("timelapse", timelapseConfig)
|
||||
|
||||
|
|
|
|||
|
|
@ -323,6 +323,11 @@ class Settings(object):
|
|||
self._dirty = False
|
||||
self._mtime = None
|
||||
|
||||
self._get_preprocessors = dict(
|
||||
controls=self._process_custom_controls
|
||||
)
|
||||
self._set_preprocessors = dict()
|
||||
|
||||
self._init_basedir(basedir)
|
||||
|
||||
if configfile is not None:
|
||||
|
|
@ -470,6 +475,30 @@ class Settings(object):
|
|||
def _get_scripts(self, script_type):
|
||||
return self._script_env.list_templates(filter_func=lambda x: x.startswith(script_type+"/"))
|
||||
|
||||
def _process_custom_controls(self, controls):
|
||||
def process_control(c):
|
||||
# shallow copy
|
||||
result = dict(c)
|
||||
|
||||
if "regex" in result and "template" in result:
|
||||
# if it's a template matcher, we need to add a key to associate with the matcher output
|
||||
import hashlib
|
||||
key_hash = hashlib.md5()
|
||||
key_hash.update(result["regex"])
|
||||
result["key"] = key_hash.hexdigest()
|
||||
|
||||
template_key_hash = hashlib.md5()
|
||||
template_key_hash.update(result["template"])
|
||||
result["template_key"] = template_key_hash.hexdigest()
|
||||
|
||||
elif "children" in result:
|
||||
# if it has children we need to process them recursively
|
||||
result["children"] = map(process_control, result["children"])
|
||||
|
||||
return result
|
||||
|
||||
return map(process_control, controls)
|
||||
|
||||
#~~ load and save
|
||||
|
||||
def load(self, migrate=False):
|
||||
|
|
@ -725,7 +754,7 @@ class Settings(object):
|
|||
|
||||
#~~ getter
|
||||
|
||||
def get(self, path, asdict=False, defaults=None, merged=False):
|
||||
def get(self, path, asdict=False, defaults=None, preprocessors=None, merged=False):
|
||||
import octoprint.util as util
|
||||
|
||||
if len(path) == 0:
|
||||
|
|
@ -734,18 +763,24 @@ class Settings(object):
|
|||
config = self._config
|
||||
if defaults is None:
|
||||
defaults = default_settings
|
||||
if preprocessors is None:
|
||||
preprocessors = self._get_preprocessors
|
||||
|
||||
while len(path) > 1:
|
||||
key = path.pop(0)
|
||||
if key in config.keys() and key in defaults.keys():
|
||||
if key in config and key in defaults:
|
||||
config = config[key]
|
||||
defaults = defaults[key]
|
||||
elif key in defaults.keys():
|
||||
elif key in defaults:
|
||||
config = {}
|
||||
defaults = defaults[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
if preprocessors and isinstance(preprocessors, dict) and key in preprocessors:
|
||||
preprocessors = preprocessors[key]
|
||||
|
||||
|
||||
k = path.pop(0)
|
||||
if not isinstance(k, (list, tuple)):
|
||||
keys = [k]
|
||||
|
|
@ -757,7 +792,7 @@ class Settings(object):
|
|||
else:
|
||||
results = []
|
||||
for key in keys:
|
||||
if key in config.keys():
|
||||
if key in config:
|
||||
value = config[key]
|
||||
if merged and key in defaults:
|
||||
value = util.dict_merge(defaults[key], value)
|
||||
|
|
@ -766,6 +801,9 @@ class Settings(object):
|
|||
else:
|
||||
value = None
|
||||
|
||||
if preprocessors and isinstance(preprocessors, dict) and key in preprocessors and callable(preprocessors[key]):
|
||||
value = preprocessors[key](value)
|
||||
|
||||
if asdict:
|
||||
results[key] = value
|
||||
else:
|
||||
|
|
@ -779,8 +817,8 @@ class Settings(object):
|
|||
else:
|
||||
return results
|
||||
|
||||
def getInt(self, path, defaults=None):
|
||||
value = self.get(path, defaults=defaults)
|
||||
def getInt(self, path, defaults=None, preprocessors=None):
|
||||
value = self.get(path, defaults=defaults, preprocessors=preprocessors)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
|
|
@ -790,8 +828,8 @@ class Settings(object):
|
|||
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
|
||||
return None
|
||||
|
||||
def getFloat(self, path, defaults=None):
|
||||
value = self.get(path, defaults=defaults)
|
||||
def getFloat(self, path, defaults=None, preprocessors=None):
|
||||
value = self.get(path, defaults=defaults, preprocessors=preprocessors)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
|
|
@ -801,8 +839,8 @@ class Settings(object):
|
|||
self._logger.warn("Could not convert %r to a valid integer when getting option %r" % (value, path))
|
||||
return None
|
||||
|
||||
def getBoolean(self, path, defaults=None):
|
||||
value = self.get(path, defaults=defaults)
|
||||
def getBoolean(self, path, defaults=None, preprocessors=None):
|
||||
value = self.get(path, defaults=defaults, preprocessors=preprocessors)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
|
|
@ -855,57 +893,9 @@ class Settings(object):
|
|||
|
||||
return script
|
||||
|
||||
def getFeedbackControls(self):
|
||||
feedbackControls = []
|
||||
for control in self.get(["controls"]):
|
||||
feedbackControls.extend(self._getFeedbackControls(control))
|
||||
return feedbackControls
|
||||
|
||||
def _getFeedbackControls(self, control=None):
|
||||
if control["type"] == "feedback_command" or control["type"] == "feedback":
|
||||
pattern = control["regex"]
|
||||
try:
|
||||
matcher = re.compile(pattern)
|
||||
return [(control["name"], matcher, control["template"])]
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
elif control["type"] == "section":
|
||||
result = []
|
||||
for c in control["children"]:
|
||||
result.extend(self._getFeedbackControls(c))
|
||||
return result
|
||||
else:
|
||||
return []
|
||||
|
||||
def getPauseTriggers(self):
|
||||
triggers = {
|
||||
"enable": [],
|
||||
"disable": [],
|
||||
"toggle": []
|
||||
}
|
||||
for trigger in self.get(["printerParameters", "pauseTriggers"]):
|
||||
try:
|
||||
regex = trigger["regex"]
|
||||
type = trigger["type"]
|
||||
if type in triggers.keys():
|
||||
# make sure regex is valid
|
||||
re.compile(regex)
|
||||
# add to type list
|
||||
triggers[type].append(regex)
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
|
||||
result = {}
|
||||
for type in triggers.keys():
|
||||
if len(triggers[type]) > 0:
|
||||
result[type] = re.compile("|".join(map(lambda x: "(%s)" % x, triggers[type])))
|
||||
return result
|
||||
|
||||
#~~ setter
|
||||
|
||||
def set(self, path, value, force=False, defaults=None):
|
||||
def set(self, path, value, force=False, defaults=None, preprocessors=None):
|
||||
if len(path) == 0:
|
||||
return
|
||||
|
||||
|
|
@ -915,6 +905,8 @@ class Settings(object):
|
|||
config = self._config
|
||||
if defaults is None:
|
||||
defaults = default_settings
|
||||
if preprocessors is None:
|
||||
preprocessors = self._set_preprocessors
|
||||
|
||||
while len(path) > 1:
|
||||
key = path.pop(0)
|
||||
|
|
@ -928,7 +920,14 @@ class Settings(object):
|
|||
else:
|
||||
return
|
||||
|
||||
if preprocessors and isinstance(preprocessors, dict) and key in preprocessors:
|
||||
preprocessors = preprocessors[key]
|
||||
|
||||
key = path.pop(0)
|
||||
|
||||
if preprocessors and isinstance(preprocessors, dict) and key in preprocessors and callable(preprocessors[key]):
|
||||
value = preprocessors[key](value)
|
||||
|
||||
if not force and key in defaults and key in config and defaults[key] == value:
|
||||
del config[key]
|
||||
self._dirty = True
|
||||
|
|
@ -939,9 +938,9 @@ class Settings(object):
|
|||
config[key] = value
|
||||
self._dirty = True
|
||||
|
||||
def setInt(self, path, value, force=False, defaults=None):
|
||||
def setInt(self, path, value, force=False, defaults=None, preprocessors=None):
|
||||
if value is None:
|
||||
self.set(path, None, force=force, defaults=defaults)
|
||||
self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors)
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -952,9 +951,9 @@ class Settings(object):
|
|||
|
||||
self.set(path, intValue, force)
|
||||
|
||||
def setFloat(self, path, value, force=False, defaults=None):
|
||||
def setFloat(self, path, value, force=False, defaults=None, preprocessors=None):
|
||||
if value is None:
|
||||
self.set(path, None, force=force, defaults=defaults)
|
||||
self.set(path, None, force=force, defaults=defaults, preprocessors=preprocessors)
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -965,13 +964,13 @@ class Settings(object):
|
|||
|
||||
self.set(path, floatValue, force)
|
||||
|
||||
def setBoolean(self, path, value, force=False, defaults=None):
|
||||
def setBoolean(self, path, value, force=False, defaults=None, preprocessors=None):
|
||||
if value is None or isinstance(value, bool):
|
||||
self.set(path, value, force=force, defaults=defaults)
|
||||
self.set(path, value, force=force, defaults=defaults, preprocessors=preprocessors)
|
||||
elif value.lower() in valid_boolean_trues:
|
||||
self.set(path, True, force=force, defaults=defaults)
|
||||
self.set(path, True, force=force, defaults=defaults, preprocessors=preprocessors)
|
||||
else:
|
||||
self.set(path, False, force=force, defaults=defaults)
|
||||
self.set(path, False, force=force, defaults=defaults, preprocessors=preprocessors)
|
||||
|
||||
def setBaseFolder(self, type, path, force=False):
|
||||
if type not in default_settings["folder"].keys():
|
||||
|
|
|
|||
|
|
@ -241,14 +241,6 @@ function DataUpdater(allViewModels) {
|
|||
|
||||
break;
|
||||
}
|
||||
case "feedbackCommandOutput": {
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("fromFeedbackCommandData")) {
|
||||
viewModel.fromFeedbackCommandData(data);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "timelapse": {
|
||||
_.each(self.allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("fromTimelapseData")) {
|
||||
|
|
|
|||
|
|
@ -87,9 +87,14 @@ $(function() {
|
|||
self.isLoading(data.flags.loading);
|
||||
};
|
||||
|
||||
self.fromFeedbackCommandData = function (data) {
|
||||
if (data.name in self.feedbackControlLookup) {
|
||||
self.feedbackControlLookup[data.name](data.output);
|
||||
self.onEventRegisteredMessageReceived = function(payload) {
|
||||
if (payload.key in self.feedbackControlLookup) {
|
||||
var outputs = self.feedbackControlLookup[payload.key];
|
||||
_.each(payload.outputs, function(value, key) {
|
||||
if (outputs.hasOwnProperty(key)) {
|
||||
outputs[key](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -122,9 +127,16 @@ $(function() {
|
|||
};
|
||||
|
||||
self._processControl = function (control) {
|
||||
if (control.type == "feedback_command" || control.type == "feedback") {
|
||||
if (control.hasOwnProperty("processed") && control.processed) {
|
||||
return control;
|
||||
}
|
||||
|
||||
if (control.hasOwnProperty("template") && control.hasOwnProperty("key") && control.hasOwnProperty("template_key") && !control.hasOwnProperty("output")) {
|
||||
control.output = ko.observable("");
|
||||
self.feedbackControlLookup[control.name] = control.output;
|
||||
if (!self.feedbackControlLookup.hasOwnProperty(control.key)) {
|
||||
self.feedbackControlLookup[control.key] = {};
|
||||
}
|
||||
self.feedbackControlLookup[control.key][control.template_key] = control.output;
|
||||
}
|
||||
|
||||
if (control.hasOwnProperty("children")) {
|
||||
|
|
@ -141,11 +153,6 @@ $(function() {
|
|||
control.input[i].slider = false;
|
||||
}
|
||||
}
|
||||
} else if (control.type == "feedback_command" || control.type == "feedback") {
|
||||
control.output = ko.observable("");
|
||||
self.feedbackControlLookup[control.name] = control.output;
|
||||
} else if (control.type == "section" || control.type == "row" || control.type == "section_row") {
|
||||
control.children = self._processControls(control.children);
|
||||
}
|
||||
|
||||
var js;
|
||||
|
|
@ -334,24 +341,10 @@ $(function() {
|
|||
};
|
||||
|
||||
self.displayMode = function (customControl) {
|
||||
switch (customControl.type) {
|
||||
case "container":
|
||||
case "section":
|
||||
return "customControls_sectionTemplate";
|
||||
case "command":
|
||||
case "commands":
|
||||
case "script":
|
||||
return "customControls_commandTemplate";
|
||||
case "parametric_command":
|
||||
case "parametric_commands":
|
||||
case "parametric_script":
|
||||
return "customControls_parametricCommandTemplate";
|
||||
case "feedback_command":
|
||||
return "customControls_feedbackCommandTemplate";
|
||||
case "feedback":
|
||||
return "customControls_feedbackTemplate";
|
||||
default:
|
||||
return "customControls_emptyTemplate";
|
||||
if (customControl.hasOwnProperty("children")) {
|
||||
return "customControls_containerTemplate";
|
||||
} else {
|
||||
return "customControls_controlTemplate";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
<div style="clear: both; display: none;" data-bind="visible: loginState.isUser, template: { name: $root.displayMode, foreach: controls }"></div>
|
||||
|
||||
<!-- Templates for custom controls -->
|
||||
<script type="text/html" id="customControls_sectionTemplate">
|
||||
<script type="text/html" id="customControls_containerTemplate">
|
||||
<!-- ko if: name -->
|
||||
<h1 data-bind="text: name"></h1>
|
||||
<!-- /ko -->
|
||||
|
|
@ -122,34 +122,28 @@
|
|||
</div>
|
||||
<!-- /ko -->
|
||||
</script>
|
||||
<script type="text/html" id="customControls_commandTemplate">
|
||||
<form class="form-inline custom_command">
|
||||
<button class="btn" data-bind="text: name, enable: $root.isCustomEnabled($data), click: function() { $root.clickCustom($data) }"></button>
|
||||
<script type="text/html" id="customControls_controlTemplate">
|
||||
<form class="form-inline">
|
||||
<!-- ko template: { name: 'customControls_controlTemplate_input', data: $data, if: $data.hasOwnProperty('input') } --><!-- /ko -->
|
||||
<!-- ko template: { name: 'customControls_controlTemplate_command', data: $data, if: $data.hasOwnProperty('command') || $data.hasOwnProperty('commands') } --><!-- /ko -->
|
||||
<!-- ko template: { name: 'customControls_controlTemplate_output', data: $data, if: $data.hasOwnProperty('output') } --><!-- /ko -->
|
||||
</form>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_feedbackCommandTemplate">
|
||||
<form class="form-inline custom_feedback_command">
|
||||
<button class="btn" data-bind="text: name, enable: $root.isCustomEnabled($data), click: function() { $root.clickCustom($data) }"></button> <span data-bind="text: output"></span>
|
||||
</form>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_feedbackTemplate">
|
||||
<div class="custom_feedback">
|
||||
<strong data-bind="text: name"></strong>: <span data-bind="text: output"></span>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_parametricCommandTemplate">
|
||||
<form class="form-inline custom_parametric_command">
|
||||
<!-- ko foreach: input -->
|
||||
<label data-bind="text: name"></label>
|
||||
<!-- ko if: slider -->
|
||||
<input type="number" style="width: 100px" data-bind="slider: {value: value, min: slider.min, max: slider.max, step: slider.step}">
|
||||
<!-- /ko -->
|
||||
<!-- ko ifnot: slider -->
|
||||
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
|
||||
<!-- /ko -->
|
||||
<script type="text/html" id="customControls_controlTemplate_input">
|
||||
<!-- ko foreach: input -->
|
||||
<label data-bind="text: name"></label>
|
||||
<!-- ko if: slider -->
|
||||
<input type="number" style="width: 100px" data-bind="slider: {value: value, min: slider.min, max: slider.max, step: slider.step}">
|
||||
<!-- /ko -->
|
||||
<button class="btn" data-bind="text: name, enable: $root.isCustomEnabled($data), click: function() { $root.clickCustom($data) }"></button>
|
||||
</form>
|
||||
<!-- ko ifnot: slider -->
|
||||
<input type="text" class="input-small" data-bind="attr: {placeholder: name}, value: value">
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</script>
|
||||
<script type="text/html" id="customControls_controlTemplate_output">
|
||||
<label data-bind="text: output"></label>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_controlTemplate_command">
|
||||
<button class="btn" data-bind="text: name, enable: $root.isCustomEnabled($data), click: function() { $root.clickCustom($data) }"></button>
|
||||
</script>
|
||||
<script type="text/html" id="customControls_emptyTemplate"><div></div></script>
|
||||
<!-- End of templates for custom controls -->
|
||||
|
|
|
|||
|
|
@ -763,9 +763,9 @@ class MachineCom(object):
|
|||
##~~ Serial monitor processing received messages
|
||||
|
||||
def _monitor(self):
|
||||
feedbackControls = settings().getFeedbackControls()
|
||||
pauseTriggers = settings().getPauseTriggers()
|
||||
feedbackErrors = []
|
||||
feedback_controls, feedback_matcher = convert_feedback_controls(settings().get(["controls"]))
|
||||
feedback_errors = []
|
||||
pause_triggers = convert_pause_triggers(settings().get(["printerParameters", "pauseTriggers"]))
|
||||
|
||||
#Open the serial port.
|
||||
if not self._openSerial():
|
||||
|
|
@ -974,35 +974,21 @@ class MachineCom(object):
|
|||
self._callback.on_comm_message(line)
|
||||
|
||||
##~~ Parsing for feedback commands
|
||||
if feedbackControls:
|
||||
for name, matcher, template in feedbackControls:
|
||||
if name in feedbackErrors:
|
||||
# we previously had an error with that one, so we'll skip it now
|
||||
continue
|
||||
try:
|
||||
match = matcher.search(line)
|
||||
if match is not None:
|
||||
formatFunction = None
|
||||
if isinstance(template, str):
|
||||
formatFunction = str.format
|
||||
elif isinstance(template, unicode):
|
||||
formatFunction = unicode.format
|
||||
|
||||
if formatFunction is not None:
|
||||
self._callback.on_comm_received_registered_message(name, formatFunction(template, *(match.groups("n/a"))))
|
||||
except:
|
||||
if not name in feedbackErrors:
|
||||
self._logger.info("Something went wrong with feedbackControl \"%s\": " % name, exc_info=True)
|
||||
feedbackErrors.append(name)
|
||||
pass
|
||||
if feedback_controls and feedback_matcher and not "_all" in feedback_errors:
|
||||
try:
|
||||
self._process_registered_message(line, feedback_matcher, feedback_controls, feedback_errors)
|
||||
except:
|
||||
# something went wrong while feedback matching
|
||||
self._logger.exception("Error while trying to apply feedback control matching, disabling it")
|
||||
feedback_errors.append("_all")
|
||||
|
||||
##~~ Parsing for pause triggers
|
||||
if pauseTriggers and not self.isStreaming():
|
||||
if "enable" in pauseTriggers.keys() and pauseTriggers["enable"].search(line) is not None:
|
||||
if pause_triggers and not self.isStreaming():
|
||||
if "enable" in pause_triggers.keys() and pause_triggers["enable"].search(line) is not None:
|
||||
self.setPause(True)
|
||||
elif "disable" in pauseTriggers.keys() and pauseTriggers["disable"].search(line) is not None:
|
||||
elif "disable" in pause_triggers.keys() and pause_triggers["disable"].search(line) is not None:
|
||||
self.setPause(False)
|
||||
elif "toggle" in pauseTriggers.keys() and pauseTriggers["toggle"].search(line) is not None:
|
||||
elif "toggle" in pause_triggers.keys() and pause_triggers["toggle"].search(line) is not None:
|
||||
self.setPause(not self.isPaused())
|
||||
|
||||
### Baudrate detection
|
||||
|
|
@ -1099,6 +1085,41 @@ class MachineCom(object):
|
|||
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
|
||||
self._log("Connection closed, closing down monitor")
|
||||
|
||||
def _process_registered_message(self, line, feedback_matcher, feedback_controls, feedback_errors):
|
||||
feedback_match = feedback_matcher.search(line)
|
||||
if feedback_match is None:
|
||||
return
|
||||
|
||||
for match_key in feedback_match.groupdict():
|
||||
try:
|
||||
feedback_key = match_key[len("group"):]
|
||||
if not feedback_key in feedback_controls or feedback_key in feedback_errors or feedback_match.group(match_key) is None:
|
||||
continue
|
||||
matched_part = feedback_match.group(match_key)
|
||||
|
||||
if feedback_controls[feedback_key]["matcher"] is None:
|
||||
continue
|
||||
|
||||
match = feedback_controls[feedback_key]["matcher"].search(matched_part)
|
||||
if match is None:
|
||||
continue
|
||||
|
||||
outputs = dict()
|
||||
for template_key, template in feedback_controls[feedback_key]["templates"].items():
|
||||
try:
|
||||
output = template.format(*match.groups())
|
||||
except KeyError:
|
||||
output = template.format(**match.groupdict())
|
||||
except:
|
||||
output = None
|
||||
|
||||
if output is not None:
|
||||
outputs[template_key] = output
|
||||
eventManager().fire(Events.REGISTERED_MESSAGE_RECEIVED, dict(key=feedback_key, matched=matched_part, outputs=outputs))
|
||||
except:
|
||||
self._logger.exception("Error while trying to match feedback control output, disabling key {key}".format(key=match_key))
|
||||
feedback_errors.append(match_key)
|
||||
|
||||
def _poll_temperature(self):
|
||||
"""
|
||||
Polls the temperature after the temperature timeout, re-enqueues itself.
|
||||
|
|
@ -1602,9 +1623,6 @@ class MachineComPrintCallback(object):
|
|||
def on_comm_file_transfer_done(self, filename):
|
||||
pass
|
||||
|
||||
def on_comm_received_registered_message(self, command, message):
|
||||
pass
|
||||
|
||||
def on_comm_force_disconnect(self):
|
||||
pass
|
||||
|
||||
|
|
@ -1861,3 +1879,70 @@ def process_gcode_line(line, offsets=None, current_tool=None):
|
|||
|
||||
return line
|
||||
|
||||
def convert_pause_triggers(configured_triggers):
|
||||
triggers = {
|
||||
"enable": [],
|
||||
"disable": [],
|
||||
"toggle": []
|
||||
}
|
||||
for trigger in configured_triggers:
|
||||
if not "regex" in trigger or not "type" in trigger:
|
||||
continue
|
||||
|
||||
try:
|
||||
regex = trigger["regex"]
|
||||
t = trigger["type"]
|
||||
if t in triggers:
|
||||
# make sure regex is valid
|
||||
re.compile(regex)
|
||||
# add to type list
|
||||
triggers[t].append(regex)
|
||||
except:
|
||||
# invalid regex or something like this, we'll just skip this entry
|
||||
pass
|
||||
|
||||
result = dict()
|
||||
for t in triggers.keys():
|
||||
if len(triggers[t]) > 0:
|
||||
result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t])))
|
||||
return result
|
||||
|
||||
|
||||
def convert_feedback_controls(configured_controls):
|
||||
def preprocess_feedback_control(control, result):
|
||||
if "key" in control and "regex" in control and "template" in control:
|
||||
# key is always the md5sum of the regex
|
||||
key = control["key"]
|
||||
|
||||
if result[key]["pattern"] is None or result[key]["matcher"] is None:
|
||||
# regex has not been registered
|
||||
try:
|
||||
result[key]["matcher"] = re.compile(control["regex"])
|
||||
result[key]["pattern"] = control["regex"]
|
||||
except Exception as exc:
|
||||
logging.getLogger(__name__).warn("Invalid regex {regex} for custom control: {exc}".format(regex=control["regex"], exc=str(exc)))
|
||||
|
||||
result[key]["templates"][control["template_key"]] = control["template"]
|
||||
|
||||
elif "children" in control:
|
||||
for c in control["children"]:
|
||||
preprocess_feedback_control(c, result)
|
||||
|
||||
def prepare_result_entry():
|
||||
return dict(pattern=None, matcher=None, templates=dict())
|
||||
|
||||
from collections import defaultdict
|
||||
feedback_controls = defaultdict(prepare_result_entry)
|
||||
|
||||
for control in configured_controls:
|
||||
preprocess_feedback_control(control, feedback_controls)
|
||||
|
||||
feedback_pattern = []
|
||||
for match_key, entry in feedback_controls.items():
|
||||
if entry["matcher"] is None or entry["pattern"] is None:
|
||||
continue
|
||||
feedback_pattern.append("(?P<group{key}>{pattern})".format(key=match_key, pattern=entry["pattern"]))
|
||||
feedback_matcher = re.compile("|".join(feedback_pattern))
|
||||
|
||||
return feedback_controls, feedback_matcher
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,29 @@ class SettingsTestCase(unittest.TestCase):
|
|||
some_int_key=1,
|
||||
some_float_key=2.5,
|
||||
some_boolean_key=True,
|
||||
preprocessed=dict(
|
||||
get="PreProcessed",
|
||||
set="PreProcessed"
|
||||
)
|
||||
)
|
||||
|
||||
self.plugin_settings = octoprint.plugin.PluginSettings(self.settings, self.plugin_key, defaults=self.defaults)
|
||||
self.get_preprocessors = dict(
|
||||
preprocessed=dict(
|
||||
get=lambda x: x.upper()
|
||||
)
|
||||
)
|
||||
|
||||
self.set_preprocessors = dict(
|
||||
preprocessed=dict(
|
||||
set=lambda x: x.lower()
|
||||
)
|
||||
)
|
||||
|
||||
self.plugin_settings = octoprint.plugin.PluginSettings(self.settings,
|
||||
self.plugin_key,
|
||||
defaults=self.defaults,
|
||||
get_preprocessors=self.get_preprocessors,
|
||||
set_preprocessors=self.set_preprocessors)
|
||||
|
||||
@data(
|
||||
("get", (["some_raw_key",],), dict(), "get"),
|
||||
|
|
@ -53,7 +73,7 @@ class SettingsTestCase(unittest.TestCase):
|
|||
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_kwargs.update(dict(defaults=dict(plugins=dict(test_plugin=self.defaults)), preprocessors=dict(plugins=dict(test_plugin=self.get_preprocessors))))
|
||||
forwarded_method.assert_called_once_with(*forwarded_args, **forwarded_kwargs)
|
||||
|
||||
@data(
|
||||
|
|
@ -90,7 +110,7 @@ class SettingsTestCase(unittest.TestCase):
|
|||
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)))
|
||||
called_method.assert_called_once_with(["plugins", self.plugin_key, "some_raw_key"], defaults=dict(plugins=dict(test_plugin=self.defaults)), preprocessors=dict(plugins=dict(test_plugin=self.get_preprocessors)))
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
|
|
@ -138,7 +158,7 @@ class SettingsTestCase(unittest.TestCase):
|
|||
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_kwargs.update(dict(defaults=dict(plugins=dict(test_plugin=self.defaults)), preprocessors=dict(plugins=dict(test_plugin=self.set_preprocessors))))
|
||||
forwarded_method.assert_called_once_with(*forwarded_args, **forwarded_kwargs)
|
||||
|
||||
@data(
|
||||
|
|
@ -176,7 +196,7 @@ class SettingsTestCase(unittest.TestCase):
|
|||
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)))
|
||||
called_method.assert_called_once_with(["plugins", self.plugin_key, "some_raw_key"], value, defaults=dict(plugins=dict(test_plugin=self.defaults)), preprocessors=dict(plugins=dict(test_plugin=self.set_preprocessors)))
|
||||
|
||||
self.assertEquals(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
|
|
@ -255,4 +275,4 @@ class SettingsTestCase(unittest.TestCase):
|
|||
try:
|
||||
self.plugin_settings.some_method("some_parameter")
|
||||
except AttributeError as e:
|
||||
self.assertTrue("Mock object has no attribute 'some_method'" in str(e))
|
||||
self.assertTrue("Mock object has no attribute 'some_method'" in str(e))
|
||||
|
|
|
|||
|
|
@ -67,4 +67,92 @@ class TestCommHelpers(unittest.TestCase):
|
|||
temperature = float(match.group(1))
|
||||
self.assertEquals(expected, temperature)
|
||||
self.assertEquals(input[:match.start(1)], actual[:match.start(1)])
|
||||
self.assertEquals(input[match.end(1):], actual[match.end(1):])
|
||||
self.assertEquals(input[match.end(1):], actual[match.end(1):])
|
||||
|
||||
def test_convert_pause_triggers(self):
|
||||
configured_triggers = [
|
||||
dict(regex="pause1", type="enable"),
|
||||
dict(regex="pause2", type="enable"),
|
||||
dict(regex="resume", type="disable"),
|
||||
dict(regex="toggle", type="toggle"),
|
||||
dict(type="enable"),
|
||||
dict(regex="regex"),
|
||||
dict(regex="regex", type="unknown")
|
||||
]
|
||||
|
||||
from octoprint.util import comm
|
||||
trigger_matchers = comm.convert_pause_triggers(configured_triggers)
|
||||
|
||||
self.assertIsNotNone(trigger_matchers)
|
||||
|
||||
self.assertIn("enable", trigger_matchers)
|
||||
self.assertEquals("(pause1)|(pause2)", trigger_matchers["enable"].pattern)
|
||||
|
||||
self.assertIn("disable", trigger_matchers)
|
||||
self.assertEquals("(resume)", trigger_matchers["disable"].pattern)
|
||||
|
||||
self.assertIn("toggle", trigger_matchers)
|
||||
self.assertEquals("(toggle)", trigger_matchers["toggle"].pattern)
|
||||
|
||||
self.assertNotIn("unknown", trigger_matchers)
|
||||
|
||||
def test_convert_feedback_controls(self):
|
||||
def md5sum(input):
|
||||
import hashlib
|
||||
m = hashlib.md5()
|
||||
m.update(input)
|
||||
return m.hexdigest()
|
||||
|
||||
temp_regex = "T:((\d*\.)\d+)"
|
||||
temp_template = "Temp: {}"
|
||||
temp2_template = "Temperature: {}"
|
||||
temp_key = md5sum(temp_regex)
|
||||
temp_template_key = md5sum(temp_template)
|
||||
temp2_template_key = md5sum(temp2_template)
|
||||
|
||||
x_regex = "X:(?P<x>\d+)"
|
||||
x_template = "X: {x}"
|
||||
x_key = md5sum(x_regex)
|
||||
x_template_key = md5sum(x_template)
|
||||
|
||||
configured_controls = [
|
||||
dict(key=temp_key, regex=temp_regex, template=temp_template, template_key=temp_template_key),
|
||||
dict(command="M117 Hello World", name="Test"),
|
||||
dict(children=[
|
||||
dict(key=x_key, regex=x_regex, template=x_template, template_key=x_template_key),
|
||||
dict(key=temp_key, regex=temp_regex, template=temp2_template, template_key=temp2_template_key)
|
||||
])
|
||||
]
|
||||
|
||||
from octoprint.util import comm
|
||||
controls, matcher = comm.convert_feedback_controls(configured_controls)
|
||||
|
||||
self.assertEquals(2, len(controls))
|
||||
|
||||
# temp_regex is used twice, so we should have two templates for it
|
||||
self.assertIn(temp_key, controls)
|
||||
temp = controls[temp_key]
|
||||
|
||||
self.assertIsNotNone(temp["matcher"])
|
||||
self.assertEquals(temp_regex, temp["matcher"].pattern)
|
||||
self.assertEquals(temp_regex, temp["pattern"])
|
||||
|
||||
self.assertEquals(2, len(temp["templates"]))
|
||||
self.assertIn(temp_template_key, temp["templates"])
|
||||
self.assertEquals(temp_template, temp["templates"][temp_template_key])
|
||||
self.assertIn(temp2_template_key, temp["templates"])
|
||||
self.assertEquals(temp2_template, temp["templates"][temp2_template_key])
|
||||
|
||||
# x_regex is used once, so we should have only one template for it
|
||||
self.assertIn(x_key, controls)
|
||||
x = controls[x_key]
|
||||
|
||||
self.assertIsNotNone(x["matcher"])
|
||||
self.assertEquals(x_regex, x["matcher"].pattern)
|
||||
self.assertEquals(x_regex, x["pattern"])
|
||||
|
||||
self.assertEquals(1, len(x["templates"]))
|
||||
self.assertIn(x_template_key, x["templates"])
|
||||
self.assertEquals(x_template, x["templates"][x_template_key])
|
||||
|
||||
self.assertEquals("(?P<group{temp_key}>{temp_regex})|(?P<group{x_key}>{x_regex})".format(**locals()), matcher.pattern)
|
||||
|
|
|
|||
Loading…
Reference in a new issue