diff --git a/src/octoprint/events.py b/src/octoprint/events.py index 70b2dce9..e176b052 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -69,6 +69,7 @@ class Events(object): CONVEYOR = "Conveyor" EJECT = "Eject" E_STOP = "EStop" + REGISTERED_MESSAGE_RECEIVED = "RegisteredMessageReceived" # Timelapse CAPTURE_START = "CaptureStart" diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index 9dde8c9b..3d4ca5dd 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -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): """ diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index ad81d436..83652324 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -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): """ diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index 031aadf4..3127e027 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -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() diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index df71be1e..93455f45 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -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( diff --git a/src/octoprint/server/util/sockjs.py b/src/octoprint/server/util/sockjs.py index 949a9efa..deffe1c1 100644 --- a/src/octoprint/server/util/sockjs.py +++ b/src/octoprint/server/util/sockjs.py @@ -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) diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index b560ab81..25f92de4 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -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(): diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index b6170d45..d547a16c 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -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")) { diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js index 404a293d..911e46d2 100644 --- a/src/octoprint/static/js/app/viewmodels/control.js +++ b/src/octoprint/static/js/app/viewmodels/control.js @@ -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"; } }; diff --git a/src/octoprint/templates/tabs/control.jinja2 b/src/octoprint/templates/tabs/control.jinja2 index 8f9ce2d0..b8cccaca 100644 --- a/src/octoprint/templates/tabs/control.jinja2 +++ b/src/octoprint/templates/tabs/control.jinja2 @@ -105,7 +105,7 @@
- - - - - + + - diff --git a/src/octoprint/util/comm.py b/src/octoprint/util/comm.py index 77066212..5a409dc3 100644 --- a/src/octoprint/util/comm.py +++ b/src/octoprint/util/comm.py @@ -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{pattern})".format(key=match_key, pattern=entry["pattern"])) + feedback_matcher = re.compile("|".join(feedback_pattern)) + + return feedback_controls, feedback_matcher + diff --git a/tests/plugin/test_settings.py b/tests/plugin/test_settings.py index 67b3d2b1..43bde77d 100644 --- a/tests/plugin/test_settings.py +++ b/tests/plugin/test_settings.py @@ -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)) \ No newline at end of file + self.assertTrue("Mock object has no attribute 'some_method'" in str(e)) diff --git a/tests/util/test_comm_helpers.py b/tests/util/test_comm_helpers.py index ff04ccb1..3015e6f6 100644 --- a/tests/util/test_comm_helpers.py +++ b/tests/util/test_comm_helpers.py @@ -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):]) \ No newline at end of file + 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\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{temp_regex})|(?P{x_regex})".format(**locals()), matcher.pattern)