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:
Gina Häußge 2015-03-10 21:49:18 +01:00
parent 7ed9cc0f87
commit 22c73d831a
13 changed files with 421 additions and 195 deletions

View file

@ -69,6 +69,7 @@ class Events(object):
CONVEYOR = "Conveyor"
EJECT = "Eject"
E_STOP = "EStop"
REGISTERED_MESSAGE_RECEIVED = "RegisteredMessageReceived"
# Timelapse
CAPTURE_START = "CaptureStart"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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