From e553a69ff5c9ecd8ed8b90fa58486bdce8e4b962 Mon Sep 17 00:00:00 2001 From: Alice Grey Date: Mon, 7 Sep 2020 11:37:01 -0500 Subject: [PATCH 1/5] Implement Configpath and Logpath Validation Added JS popup handler. Added configpath and logpath validation when settings are saved. Add logpath validation Change file checking method Check new values instead of old settings on save --- octoprint_klipper/__init__.py | 57 ++++-- octoprint_klipper/static/js/klipper.js | 256 +++++++++++++------------ 2 files changed, 179 insertions(+), 134 deletions(-) diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index d24b3ef..2a0d216 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -81,41 +81,55 @@ class KlipperPlugin( def on_settings_load(self): data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - filepath = os.path.expanduser( + configpath = os.path.expanduser( self._settings.get(["configuration", "configpath"]) ) + + logpath = os.path.expanduser( + self._settings.get(["configuration", "logpath"]) + ) + try: - f = open(filepath, "r") + f = open(configpath, "r") data["config"] = f.read() f.close() except IOError: self._logger.error( - "Error: Klipper config file not found at: {}".format(filepath) + "Error: Klipper config file not found at: {}".format(configpath) ) return data def on_settings_save(self, data): + if self.keyexists(data, "configuration", "configpath"): + configpath = os.path.expanduser( + data["configuration"]["configpath"] + ) + self.checkFile("config",configpath) + + if self.keyexists(data, "configuration", "logpath"): + logpath = os.path.expanduser( + data["configuration"]["logpath"] + ) + self.checkFile("log",logpath) + if "config" in data: try: - filepath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) data["config"] = data["config"].encode('utf-8') - f = open(filepath, "w") + f = open(configpath, "w") f.write(data["config"]) f.close() - self._logger.info( - "Writing Klipper config to {}".format(filepath) + self._logger.error( + "Writing Klipper config to {}".format(configpath) ) # Restart klipply to reload config self._printer.commands(self._settings.get(["configuration", "reload_command"])) self.logInfo("Reloading Klipper Configuration.") except IOError: self._logger.error( - "Error: Couldn't write Klipper config file: {}".format(filepath) + "Error: Couldn't write Klipper config file: {}".format(configpath) ) - data.pop("config", None) # we dont want to write the klipper conf to the octoprint settings + data.pop("config", None) # we dont want to write the klipper conf to the octoprint settings else: octoprint.plugin.SettingsPlugin.on_settings_save(self, data) @@ -324,13 +338,14 @@ class KlipperPlugin( ) #-- Helpers - def sendMessage(self, type, subtype, payload): + def sendMessage(self, type, subtype, title, payload): self._plugin_manager.send_plugin_message( self._identifier, dict( time=datetime.datetime.now().strftime("%H:%M:%S"), type=type, subtype=subtype, + title=title, payload=payload ) ) @@ -339,13 +354,25 @@ class KlipperPlugin( self._printer.commands("STATUS") def updateStatus(self, type, status): - self.sendMessage("status", type, status) + self.sendMessage("status", type, status, status) def logInfo(self, message): - self.sendMessage("log", "info", message) + self.sendMessage("log", "info", message, message) def logError(self, error): - self.sendMessage("log", "error", error) + self.sendMessage("log", "error", error, error) + + def checkFile(self,filetype,filepath): + if not os.path.isfile(filepath): + self.sendMessage("errorPopUp","warning", "OctoKlipper Settings", "Klipper " + filepath + " does not exist!") + + def keyexists(self, dict, key1, key2): + try: + dict[key1][key2] + except KeyError: + return False + else: + return True __plugin_name__ = "OctoKlipper" __plugin_pythoncompat__ = ">=2.7,<4" diff --git a/octoprint_klipper/static/js/klipper.js b/octoprint_klipper/static/js/klipper.js index 6335409..8d9d1ff 100644 --- a/octoprint_klipper/static/js/klipper.js +++ b/octoprint_klipper/static/js/klipper.js @@ -14,126 +14,144 @@ // along with this program. If not, see . $(function() { - function KlipperViewModel(parameters) { - var self = this; + function KlipperViewModel(parameters) { + var self = this; - self.settings = parameters[0]; - self.loginState = parameters[1]; - self.connectionState = parameters[2]; - self.levelingViewModel = parameters[3]; - self.paramMacroViewModel = parameters[4]; - - self.shortStatus = ko.observable(); - self.logMessages = ko.observableArray(); - - self.showLevelingDialog = function() { - var dialog = $("#klipper_leveling_dialog"); - dialog.modal({ - show: 'true', - backdrop: 'static', - keyboard: false - }); - self.levelingViewModel.initView(); - } - - self.showPidTuningDialog = function() { - var dialog = $("#klipper_pid_tuning_dialog"); - dialog.modal({ - show: 'true', - backdrop: 'static', - keyboard: false - }); - } - - self.showOffsetDialog = function() { - var dialog = $("#klipper_offset_dialog"); - dialog.modal({ - show: 'true', - backdrop: 'static' - }); - } - - self.showGraphDialog = function() { - var dialog = $("#klipper_graph_dialog"); - dialog.modal({ - show: 'true', - minHeight: "500px", - maxHeight: "600px" - }); - } - - self.executeMacro = function(macro) { - var paramObjRegex = /{(.*?)}/g; - - if (macro.macro().match(paramObjRegex) == null) { - OctoPrint.control.sendGcode( - // Use .split to create an array of strings which is sent to - // OctoPrint.control.sendGcode instead of a single string. - macro.macro().split(/\r\n|\r|\n/) - ); - } else { - self.paramMacroViewModel.process(macro); - - var dialog = $("#klipper_macro_dialog"); - dialog.modal({ - show: 'true', - backdrop: 'static' - }); - } - } - - self.onGetStatus = function() { - OctoPrint.control.sendGcode("Status") - } - - self.onRestartFirmware = function() { - OctoPrint.control.sendGcode("FIRMWARE_RESTART") - }; - - self.onRestartHost = function() { - OctoPrint.control.sendGcode("RESTART") - }; - - self.onAfterBinding = function() { - self.connectionState.selectedPort(self.settings.settings.plugins.klipper.connection.port()); - } - - self.onDataUpdaterPluginMessage = function(plugin, message) { - if(plugin == "klipper") { - if(message["type"] == "status") { - self.shortStatus(message["payload"]); - } else { - self.logMessage(message["time"], message["subtype"], message["payload"]); - } - } - } + self.settings = parameters[0]; + self.loginState = parameters[1]; + self.connectionState = parameters[2]; + self.levelingViewModel = parameters[3]; + self.paramMacroViewModel = parameters[4]; + + self.shortStatus = ko.observable(); + self.logMessages = ko.observableArray(); - self.logMessage = function(timestamp, type, message) { - self.logMessages.push({ - time: timestamp, - type: type, - msg: message.replace(/\n/gi, "
")} - ); - } - - self.onClearLog = function() { - self.logMessages.removeAll(); - }; - - self.isActive = function() { - return self.connectionState.isOperational() && self.loginState.isUser(); - } - } + self.showPopUp = function(popupType, popupTitle, message){ + var title = popupType.toUpperCase() + ": " + popupTitle; + new PNotify({ + title: title, + text: message, + type: popupType, + hide: false + }); + }; + + self.showLevelingDialog = function() { + var dialog = $("#klipper_leveling_dialog"); + dialog.modal({ + show: 'true', + backdrop: 'static', + keyboard: false + }); + self.levelingViewModel.initView(); + }; + + self.showPidTuningDialog = function() { + var dialog = $("#klipper_pid_tuning_dialog"); + dialog.modal({ + show: 'true', + backdrop: 'static', + keyboard: false + }); + }; + + self.showOffsetDialog = function() { + var dialog = $("#klipper_offset_dialog"); + dialog.modal({ + show: 'true', + backdrop: 'static' + }); + }; + + self.showGraphDialog = function() { + var dialog = $("#klipper_graph_dialog"); + dialog.modal({ + show: 'true', + minHeight: "500px", + maxHeight: "600px" + }); + }; + + self.executeMacro = function(macro) { + var paramObjRegex = /{(.*?)}/g; + + if (macro.macro().match(paramObjRegex) == null) { + OctoPrint.control.sendGcode( + // Use .split to create an array of strings which is sent to + // OctoPrint.control.sendGcode instead of a single string. + macro.macro().split(/\r\n|\r|\n/) + ); + } else { + self.paramMacroViewModel.process(macro); + + var dialog = $("#klipper_macro_dialog"); + dialog.modal({ + show: 'true', + backdrop: 'static' + }); + } + }; + + self.onGetStatus = function() { + OctoPrint.control.sendGcode("Status") + }; + + self.onRestartFirmware = function() { + OctoPrint.control.sendGcode("FIRMWARE_RESTART") + }; + + self.onRestartHost = function() { + OctoPrint.control.sendGcode("RESTART") + }; + + self.onAfterBinding = function() { + self.connectionState.selectedPort(self.settings.settings.plugins.klipper.connection.port()); + }; + + self.onDataUpdaterPluginMessage = function(plugin, data) { + if(plugin == "klipper") { + if ("warningPopUp" == data.type){ + self.showPopUp(data.subtype, data.title, data.payload); + return; + } + if ("errorPopUp" == data.type){ + self.showPopUp(data.subtype, data.title, data.payload); + return; + } + if(data.type == "status") { + self.shortStatus(data.payload); + } else { + self.logMessage(data.time, data.subtype, data.payload); + } + } + }; - OCTOPRINT_VIEWMODELS.push({ - construct: KlipperViewModel, - dependencies: [ - "settingsViewModel", - "loginStateViewModel", - "connectionViewModel", - "klipperLevelingViewModel", - "klipperMacroDialogViewModel" - ], - elements: ["#tab_plugin_klipper_main", "#sidebar_plugin_klipper", "#navbar_plugin_klipper"] - }); + self.logMessage = function(timestamp, type, message) { + self.logMessages.push({ + time: timestamp, + type: type, + msg: message.replace(/\n/gi, "
")} + ); + }; + + self.onClearLog = function() { + self.logMessages.removeAll(); + }; + + self.isActive = function() { + return self.connectionState.isOperational() && self.loginState.isUser(); + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperViewModel, + dependencies: [ + "settingsViewModel", + "loginStateViewModel", + "connectionViewModel", + "klipperLevelingViewModel", + "klipperMacroDialogViewModel" + ], + elements: ["#tab_plugin_klipper_main", "#sidebar_plugin_klipper", "#navbar_plugin_klipper"] + }); }); From 1d8a6cb35296cd6415ab317f2f31ad1a885f6c77 Mon Sep 17 00:00:00 2001 From: Oliver Fawcett-Griffiths Date: Sun, 4 Oct 2020 16:31:10 +1300 Subject: [PATCH 2/5] Implement further config validation Basically just parse the config file before we save it, ensuring that it can be parsed by `configparser` - the same parser used by Klipper. This PR depende on the changes from https://github.com/AliceGrey/OctoprintKlipperPlugin/pull/8 in order to send a warning popup when validation fails. --- octoprint_klipper/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index 2a0d216..fc35621 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -22,6 +22,7 @@ import os from octoprint.util.comm import parse_firmware_line from .modules import KlipperLogAnalyzer import flask +import configparser class KlipperPlugin( octoprint.plugin.StartupPlugin, @@ -116,6 +117,17 @@ class KlipperPlugin( try: data["config"] = data["config"].encode('utf-8') + # Basic validation of config file - makes sure it parses + try: + parser = configparser.RawConfigParser() + parser.read_string(data["config"]) + except configParser.Error as error: + self._logger.error( + "Error: Invalid Klipper config file: {}".format(str(error)) + ) + self.sendMessage("errorPopUp","warning", "OctoKlipper Settings", "Invalid Klipper config file: " + str(error)) + + f = open(configpath, "w") f.write(data["config"]) f.close() From bc6302e967a44a372402bc904d041edbc9830353 Mon Sep 17 00:00:00 2001 From: thelastWallE <12502210+thelastWallE@users.noreply.github.com> Date: Tue, 16 Mar 2021 19:06:16 +0100 Subject: [PATCH 3/5] *prepare workspace *bugfixes *testing --- .editorconfig | 15 +++++++--- .gitignore | 3 +- octoprint_klipper/__init__.py | 25 ++++++++++++---- octoprint_klipper/static/css/klipper.css | 16 +++++----- octoprint_klipper/static/js/klipper.js | 30 ++++++++----------- octoprint_klipper/static/less/klipper.less | 5 ---- .../templates/klipper_sidebar.jinja2 | 9 ++---- 7 files changed, 54 insertions(+), 49 deletions(-) delete mode 100644 octoprint_klipper/static/less/klipper.less diff --git a/.editorconfig b/.editorconfig index 82c8e05..540df65 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,10 +8,17 @@ end_of_line = lf charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true +indent_style = space +max_line_length = 90 [**.py] -indent_style = tab +indent_size = 3 -[**.js] -indent_style = space -indent_size = 4 +[**.yaml] +indent_size = 2 + +[/static/less/**.less] +indent_size = 2 + +[**.jinja2] +indent_size = 3 diff --git a/.gitignore b/.gitignore index 5d59104..b46a89f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ dist .DS_Store *.zip -*.bak \ No newline at end of file +*.bak +.vscode diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index 4b50863..5d9692c 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -59,7 +59,7 @@ class KlipperPlugin( "description": gettext("Allows to config klipper"), "default_groups": [ADMIN_GROUP], "dangerous": True, - "roles": ["admin"], + "roles": ["admin"] }, { "key": "MACRO", @@ -67,10 +67,10 @@ class KlipperPlugin( "description": gettext("Allows to use klipper macros"), "default_groups": [ADMIN_GROUP], "dangerous": True, - "roles": ["admin"], + "roles": ["admin"] }, ] - + def get_settings_defaults(self): return dict( connection = dict( @@ -118,6 +118,20 @@ class KlipperPlugin( ) return data + def reloadConfigfile(self): + data = octoprint.plugin.SettingsPlugin.on_settings_load(self) + + filepath = os.path.expanduser( + self._settings.get(["configuration", "configpath"])) + try: + f = open(filepath, "r", encoding="utf-8") + data["config"] = f.read() + f.close() + except IOError: + self._logger.error( + "Error: Klipper config file not found at: {}".format(filepath)) + return data + def on_settings_save(self, data): if "config" in data: try: @@ -346,8 +360,8 @@ class KlipperPlugin( repo="OctoprintKlipperPlugin", pip="https://github.com/thelastWallE/OctoprintKlipperPlugin/archive/{target_version}.zip", stable_branch=dict( - name="Stable", - branch="master", + name="Stable", + branch="master", comittish=["master"] ), prerelease_branches=[ @@ -396,4 +410,3 @@ def __plugin_load__(): "octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode, "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information } - diff --git a/octoprint_klipper/static/css/klipper.css b/octoprint_klipper/static/css/klipper.css index 841c7d4..df1a37f 100644 --- a/octoprint_klipper/static/css/klipper.css +++ b/octoprint_klipper/static/css/klipper.css @@ -38,10 +38,6 @@ margin-bottom: 6px; } -#level .add-on { - padding: 5px; -} - #level .controls { padding: 1px; } @@ -80,7 +76,7 @@ ul#klipper-settings { #tab_plugin_klipper_main .span4 { flex-grow: 0; flex-shrink: 1; - max-width: 13%; + /* max-width: 13%; */ float: left; box-sizing: border-box; } @@ -109,15 +105,17 @@ ul#klipper-settings { } #macros #item.control-group { - margin-bottom: 5px; - border: 1px solid #ddd; + margin-bottom: 2px; + border: 2px solid #ccc; border-radius: 3px; - background-color: #eeeeef; + background-color: #eeeeee; color: #333; + padding-bottom: 2px; + padding-top: 2px; } #klipper_graph_dialog form { - margin: 0; + margin: 0; } #klipper_graph_dialog .graph-footer { diff --git a/octoprint_klipper/static/js/klipper.js b/octoprint_klipper/static/js/klipper.js index 1bb68c5..e899629 100644 --- a/octoprint_klipper/static/js/klipper.js +++ b/octoprint_klipper/static/js/klipper.js @@ -31,7 +31,7 @@ $(function () { dialog.modal({ show: "true", backdrop: "static", - keyboard: false, + keyboard: false }); self.levelingViewModel.initView(); }; @@ -41,7 +41,7 @@ $(function () { dialog.modal({ show: "true", backdrop: "static", - keyboard: false, + keyboard: false }); }; @@ -49,7 +49,7 @@ $(function () { var dialog = $("#klipper_offset_dialog"); dialog.modal({ show: "true", - backdrop: "static", + backdrop: "static" }); }; @@ -58,7 +58,7 @@ $(function () { dialog.modal({ show: "true", minHeight: "500px", - maxHeight: "600px", + maxHeight: "600px" }); }; @@ -79,7 +79,7 @@ $(function () { var dialog = $("#klipper_macro_dialog"); dialog.modal({ show: "true", - backdrop: "static", + backdrop: "static" }); } }; @@ -124,7 +124,7 @@ $(function () { self.logMessages.push({ time: timestamp, type: type, - msg: message.replace(/\n/gi, "
"), + msg: message.replace(/\n/gi, "
") }); }; @@ -137,14 +137,12 @@ $(function () { }; self.hasRight = function (right_role, type) { - var arg = eval( - "self.access.permissions.PLUGIN_KLIPPER_" + right_role - ); + var arg = eval("self.access.permissions.PLUGIN_KLIPPER_" + right_role); if (type == "Ko") { return self.loginState.hasPermissionKo(arg); - } - return self.loginState.hasPermission(arg); + } + return self.loginState.hasPermission(arg); }; // OctoKlipper settings link @@ -155,9 +153,7 @@ $(function () { $("li#settings_plugin_klipper_link a").click(); if (profile_type) { var query = - "#klipper-settings a[data-profile-type='" + - profile_type + - "']"; + "#klipper-settings a[data-profile-type='" + profile_type + "']"; $(query).click(); } }; @@ -171,12 +167,12 @@ $(function () { "connectionViewModel", "klipperLevelingViewModel", "klipperMacroDialogViewModel", - "accessViewModel", + "accessViewModel" ], elements: [ "#tab_plugin_klipper_main", "#sidebar_plugin_klipper", - "#navbar_plugin_klipper", - ], + "#navbar_plugin_klipper" + ] }); }); diff --git a/octoprint_klipper/static/less/klipper.less b/octoprint_klipper/static/less/klipper.less deleted file mode 100644 index 602468b..0000000 --- a/octoprint_klipper/static/less/klipper.less +++ /dev/null @@ -1,5 +0,0 @@ -#tab_plugin_klipper iframe { - width: 100%; - height: 600px; - border: 1px solid #808080; -} \ No newline at end of file diff --git a/octoprint_klipper/templates/klipper_sidebar.jinja2 b/octoprint_klipper/templates/klipper_sidebar.jinja2 index f61e193..d132e8d 100644 --- a/octoprint_klipper/templates/klipper_sidebar.jinja2 +++ b/octoprint_klipper/templates/klipper_sidebar.jinja2 @@ -7,17 +7,12 @@
-
- -
-
-
- +
-
\ No newline at end of file + From c1e2cb5ae0fb6616225ba6f46d0067f0d24b06ab Mon Sep 17 00:00:00 2001 From: thelastWallE <12502210+thelastWallE@users.noreply.github.com> Date: Tue, 16 Mar 2021 19:32:01 +0100 Subject: [PATCH 4/5] -indentation -css --- octoprint_klipper/__init__.py | 24 ++++++++++++------------ octoprint_klipper/static/css/klipper.css | 6 ++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index 5d9692c..66024c3 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -118,19 +118,19 @@ class KlipperPlugin( ) return data - def reloadConfigfile(self): - data = octoprint.plugin.SettingsPlugin.on_settings_load(self) + def reloadConfigfile(self): + data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - filepath = os.path.expanduser( - self._settings.get(["configuration", "configpath"])) - try: - f = open(filepath, "r", encoding="utf-8") - data["config"] = f.read() - f.close() - except IOError: - self._logger.error( - "Error: Klipper config file not found at: {}".format(filepath)) - return data + filepath = os.path.expanduser( + self._settings.get(["configuration", "configpath"])) + try: + f = open(filepath, "r", encoding="utf-8") + data["config"] = f.read() + f.close() + except IOError: + self._logger.error( + "Error: Klipper config file not found at: {}".format(filepath)) + return data def on_settings_save(self, data): if "config" in data: diff --git a/octoprint_klipper/static/css/klipper.css b/octoprint_klipper/static/css/klipper.css index df1a37f..3dd3026 100644 --- a/octoprint_klipper/static/css/klipper.css +++ b/octoprint_klipper/static/css/klipper.css @@ -42,11 +42,11 @@ padding: 1px; } -ul#klipper-settings { +ul#klipper-settings { margin: 0; } -#klipper-settings a{ +#klipper-settings a{ margin: 5px; } @@ -62,8 +62,6 @@ ul#klipper-settings { } #tab_plugin_klipper_main .span8 { - min-width: 50%; - width: unset; flex-grow: 8; float: left; box-sizing: border-box; From 415da30ddfa53a2cfb04b91888370ff8a4137709 Mon Sep 17 00:00:00 2001 From: thelastWallE <12502210+thelastWallE@users.noreply.github.com> Date: Fri, 26 Mar 2021 23:39:08 +0100 Subject: [PATCH 5/5] +reload configfile as the settings dialog is opened +Syntax Highlighting for Configfile +Show logfile content on the Performance Graph Dialog +Check parsing of configfile before saving --- .editorconfig | 2 +- octoprint_klipper/__init__.py | 884 ++++++++++-------- octoprint_klipper/goldenprinter.cfg | 519 ++++++++++ .../modules/KlipperLogAnalyzer.py | 20 +- octoprint_klipper/static/css/klipper.css | 23 +- octoprint_klipper/static/js/klipper.js | 103 +- octoprint_klipper/static/js/klipper_graph.js | 68 +- .../static/js/klipper_settings.js | 144 ++- .../templates/klipper_graph_dialog.jinja2 | 7 +- .../templates/klipper_settings.jinja2 | 58 +- .../templates/klipper_tab_main.jinja2 | 8 +- 11 files changed, 1275 insertions(+), 561 deletions(-) create mode 100644 octoprint_klipper/goldenprinter.cfg diff --git a/.editorconfig b/.editorconfig index 540df65..cd7618d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,7 +12,7 @@ indent_style = space max_line_length = 90 [**.py] -indent_size = 3 +indent_size = 4 [**.yaml] indent_size = 2 diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index f005afc..245d365 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from __future__ import absolute_import, division, print_function, unicode_literals import datetime import logging import octoprint.plugin @@ -24,429 +25,516 @@ from octoprint.util.comm import parse_firmware_line from octoprint.access.permissions import Permissions, ADMIN_GROUP, USER_GROUP from .modules import KlipperLogAnalyzer import flask -import configparser from flask_babel import gettext +try: + import configparser +except ImportError: + import ConfigParser as configparser + class KlipperPlugin( - octoprint.plugin.StartupPlugin, - octoprint.plugin.TemplatePlugin, - octoprint.plugin.SettingsPlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.SimpleApiPlugin, - octoprint.plugin.EventHandlerPlugin): + octoprint.plugin.StartupPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.SettingsPlugin, + octoprint.plugin.AssetPlugin, + octoprint.plugin.SimpleApiPlugin, + octoprint.plugin.EventHandlerPlugin): - _parsing_response = False - _message = "" + _parsing_response = False + _message = "" - #-- Startup Plugin + # -- Startup Plugin - def on_after_startup(self): - klipper_port = self._settings.get(["connection", "port"]) - additional_ports = self._settings.global_get(["serial", "additionalPorts"]) + def on_after_startup(self): + klipper_port = self._settings.get(["connection", "port"]) + additional_ports = self._settings.global_get( + ["serial", "additionalPorts"]) - if klipper_port not in additional_ports: - additional_ports.append(klipper_port) - self._settings.global_set(["serial", "additionalPorts"], additional_ports) - self._settings.save() - self._logger.info("Added klipper serial port {} to list of additional ports.".format(klipper_port)) + if klipper_port not in additional_ports: + additional_ports.append(klipper_port) + self._settings.global_set( + ["serial", "additionalPorts"], additional_ports) + self._settings.save() + self._logger.info( + "Added klipper serial port {} to list of additional ports.".format(klipper_port)) - #-- Settings Plugin + # -- Settings Plugin - def get_additional_permissions(self, *args, **kwargs): + def get_additional_permissions(self, *args, **kwargs): return [ - { - "key": "CONFIG", - "name": "Config Klipper", - "description": gettext("Allows to config klipper"), - "default_groups": [ADMIN_GROUP], - "dangerous": True, - "roles": ["admin"] + { + "key": "CONFIG", + "name": "Config Klipper", + "description": gettext("Allows to config klipper"), + "default_groups": [ADMIN_GROUP], + "dangerous": True, + "roles": ["admin"] }, { - "key": "MACRO", - "name": "Use Klipper Macros", - "description": gettext("Allows to use klipper macros"), - "default_groups": [ADMIN_GROUP], - "dangerous": True, - "roles": ["admin"] + "key": "MACRO", + "name": "Use Klipper Macros", + "description": gettext("Allows to use klipper macros"), + "default_groups": [ADMIN_GROUP], + "dangerous": True, + "roles": ["admin"] }, ] - def get_settings_defaults(self): - return dict( - connection = dict( - port="/tmp/printer", - replace_connection_panel=True - ), - macros = [dict( - name="E-Stop", - macro="M112", - sidebar=True, - tab=True - )], - probe = dict( - height=0, - lift=5, - speed_xy=1500, - speed_z=500, - points=[dict( - name="point-1", - x=0, - y=0 - )] - ), - configuration = dict( - configpath="~/printer.cfg", - logpath="/tmp/klippy.log", - reload_command="RESTART", - navbar=True - ) - ) - - def on_settings_load(self): - data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) - - logpath = os.path.expanduser( - self._settings.get(["configuration", "logpath"]) - ) - - try: - f = open(configpath, "r") - - data["config"] = f.read() - f.close() - except IOError: - self._logger.error( - "Error: Klipper config file not found at: {}".format(configpath) - ) - return data - - def reloadConfigfile(self): - data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - - filepath = os.path.expanduser( - self._settings.get(["configuration", "configpath"])) - try: - f = open(filepath, "r", encoding="utf-8") - data["config"] = f.read() - f.close() - except IOError: - self._logger.error( - "Error: Klipper config file not found at: {}".format(filepath)) - return data - - def on_settings_save(self, data): - if self.keyexists(data, "configuration", "configpath"): - configpath = os.path.expanduser( - data["configuration"]["configpath"] - ) - self.checkFile("config",configpath) - - if self.keyexists(data, "configuration", "logpath"): - logpath = os.path.expanduser( - data["configuration"]["logpath"] - ) - self.checkFile("log",logpath) - - if "config" in data: - try: - data["config"] = data["config"].encode('utf-8') - - # Basic validation of config file - makes sure it parses - try: - parser = configparser.RawConfigParser() - parser.read_string(data["config"]) - except configParser.Error as error: - self._logger.error( - "Error: Invalid Klipper config file: {}".format(str(error)) - ) - self.sendMessage("errorPopUp","warning", "OctoKlipper Settings", "Invalid Klipper config file: " + str(error)) - - - f = open(configpath, "w") - - f.write(data["config"]) - f.close() - self._logger.error( - "Writing Klipper config to {}".format(configpath) - ) - # Restart klipply to reload config - self._printer.commands(self._settings.get(["configuration", "reload_command"])) - self.logInfo("Reloading Klipper Configuration.") - except IOError: - self._logger.error( - "Error: Couldn't write Klipper config file: {}".format(configpath) - ) - data.pop("config", None) # we dont want to write the klipper conf to the octoprint settings - else: - octoprint.plugin.SettingsPlugin.on_settings_save(self, data) - - def get_settings_restricted_paths(self): - return dict( - admin=[ - ["connection", "port"], - ["configuration", "configpath"], - ["configuration", "replace_connection_panel"] - ], - user=[ - ["macros"], - ["probe"] - ] - ) - - def get_settings_version(self): - return 2 - - def on_settings_migrate(self, target, current): - if current is None: - settings = self._settings - - if settings.has(["serialport"]): - settings.set(["connection", "port"], settings.get(["serialport"]) ) - settings.remove(["serialport"]) - - if settings.has(["replace_connection_panel"]): - settings.set( - ["connection", "replace_connection_panel"], - settings.get(["replace_connection_panel"]) - ) - settings.remove(["replace_connection_panel"]) - - if settings.has(["probeHeight"]): - settings.set(["probe", "height"], settings.get(["probeHeight"])) - settings.remove(["probeHeight"]) - - if settings.has(["probeLift"]): - settings.set(["probe", "lift"], settings.get(["probeLift"])) - settings.remove(["probeLift"]) - - if settings.has(["probeSpeedXy"]): - settings.set(["probe", "speed_xy"], settings.get(["probeSpeedXy"])) - settings.remove(["probeSpeedXy"]) - - if settings.has(["probeSpeedZ"]): - settings.set(["probe", "speed_z"], settings.get(["probeSpeedZ"])) - settings.remove(["probeSpeedZ"]) - - if settings.has(["probePoints"]): - points = settings.get(["probePoints"]) - points_new = [] - for p in points: - points_new.append(dict(name="", x=int(p["x"]), y=int(p["y"]), z=0)) - settings.set(["probe", "points"], points_new) - settings.remove(["probePoints"]) - - if settings.has(["configPath"]): - settings.set(["config_path"], settings.get(["configPath"])) - settings.remove(["configPath"]) - - #-- Template Plugin - - def get_template_configs(self): - return [ - dict(type="navbar", custom_bindings=True), - dict(type="settings", custom_bindings=True), - dict( - type="generic", - name="Assisted Bed Leveling", - template="klipper_leveling_dialog.jinja2", - custom_bindings=True - ), - dict( - type="generic", - name="PID Tuning", - template="klipper_pid_tuning_dialog.jinja2", - custom_bindings=True - ), - dict( - type="generic", - name="Coordinate Offset", - template="klipper_offset_dialog.jinja2", - custom_bindings=True - ), - dict( - type="tab", - name="Klipper", - template="klipper_tab_main.jinja2", - suffix="_main", - custom_bindings=True - ), - dict(type="sidebar", - custom_bindings=True, - icon="rocket", - replaces= "connection" if self._settings.get_boolean(["connection", "replace_connection_panel"]) else "" - ), - dict( - type="generic", - name="Performance Graph", - template="klipper_graph_dialog.jinja2", - custom_bindings=True - ), - dict( - type="generic", - name="Macro Dialog", - template="klipper_param_macro_dialog.jinja2", - custom_bindings=True - ) - ] - - #-- Asset Plugin - - def get_assets(self): - return dict( - js=["js/klipper.js", - "js/klipper_settings.js", - "js/klipper_leveling.js", - "js/klipper_pid_tuning.js", - "js/klipper_offset.js", - "js/klipper_param_macro.js", - "js/klipper_graph.js" - ], - css=["css/klipper.css"], - less=["css/klipper.less"] - ) - - #-- Event Handler Plugin - - def on_event(self, event, payload): - if "UserLoggedIn" == event: - self.updateStatus("info","Klipper: Standby") - if "Connecting" == event: - self.updateStatus("info", "Klipper: Connecting ...") - elif "Connected" == event: - self.updateStatus("info", "Klipper: Connected to host") - self.logInfo("Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"])) - elif "Disconnected" == event: - self.updateStatus("info", "Klipper: Disconnected from host") - elif "Error" == event: - self.updateStatus("error", "Klipper: Error") - self.logError(payload["error"]) - - #-- GCODE Hook - - def on_parse_gcode(self, comm, line, *args, **kwargs): - if "FIRMWARE_VERSION" in line: - printerInfo = parse_firmware_line(line) - if "FIRMWARE_VERSION" in printerInfo: - self.logInfo("Firmware version: {}".format(printerInfo["FIRMWARE_VERSION"])) - elif "//" in line: - self._message = self._message + line.strip('/') - if not self._parsing_response: - self.updateStatus("info", self._message) - self._parsing_response = True - else: - if self._parsing_response: - self._parsing_response = False - self.logInfo(self._message) - self._message = "" - if "!!" in line: - msg = line.strip('!') - self.updateStatus("error", msg) - self.logError(msg) - return line - - def get_api_commands(self): - return dict( - listLogFiles=[], - getStats=["logFile"], - loadConfig=["configFile"] - ) - - def on_api_command(self, command, data): - if command == "listLogFiles": - files = [] - for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"): - filesize = os.path.getsize(f) - files.append(dict( - name=os.path.basename(f) + " ({:.1f} KB)".format(filesize / 1000.0), - file=f, - size=filesize - )) - return flask.jsonify(data=files) - elif command == "getStats": - if "logFile" in data: - log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer(data["logFile"]) - return flask.jsonify(log_analyzer.analyze()) - elif command == "loadConfig": - kc = Parser() - sections = kc.load(data["configFile"]) - return flask.jsonify(sections) - - def get_update_information(self): - return dict( - klipper=dict( - displayName=self._plugin_name, - displayVersion=self._plugin_version, - type="github_release", - current=self._plugin_version, - user="thelastWallE", - repo="OctoprintKlipperPlugin", - pip="https://github.com/thelastWallE/OctoprintKlipperPlugin/archive/{target_version}.zip", - stable_branch=dict( - name="Stable", - branch="master", - comittish=["master"] + def get_settings_defaults(self): + return dict( + connection=dict( + port="/tmp/printer", + replace_connection_panel=True ), - prerelease_branches=[ - dict( - name="Release Candidate", - branch="rc", - comittish=["rc", "master"], - ) + macros=[dict( + name="E-Stop", + macro="M112", + sidebar=True, + tab=True + )], + probe=dict( + height=0, + lift=5, + speed_xy=1500, + speed_z=500, + points=[dict( + name="point-1", + x=0, + y=0 + )] + ), + configuration=dict( + configpath="~/printer.cfg", + logpath="/tmp/klippy.log", + reload_command="RESTART", + navbar=True + ) + ) + + def on_settings_load(self): + data = octoprint.plugin.SettingsPlugin.on_settings_load(self) + + configpath = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + + try: + f = open(configpath, "r") + data["config"] = f.read() + f.close() + except IOError: + self._logger.error( + "Error: Klipper config file not found at: {}".format( + configpath) + ) + else: + self.send_message("reload", "config", "", data["config"]) + # send the configdata to frontend to update ace editor + return data + + def on_settings_save(self, data): + + self.log_console( + "debug", + "Save klipper configs" + ) + + if "config" in data: + try: + if sys.version_info[0] < 3: + data["config"] = data["config"].encode('utf-8') + + # check for configpath if it was changed during changing of the configfile + if self.key_exist(data, "configuration", "configpath"): + configpath = os.path.expanduser( + data["configuration"]["configpath"] + ) + else: + # if the configpath was not changed during changing the printer.cfg. Then the configpath would not be in data[] + configpath = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + + if self.file_exist(configpath) and self.validate_configfile(data["config"]): + f = open(configpath, "w") + f.write(data["config"]) + f.close() + # Restart klippy to reload config + self._printer.commands(self._settings.get( + ["configuration", "reload_command"])) + self.log_info("Reloading Klipper Configuration.") + self.log_console( + "debug", + ("Writing Klipper config to {}".format(configpath)) + ) + + except IOError: + self._logger.error( + "Error: Couldn't write Klipper config file: {}".format( + configpath) + ) + else: + # we dont want to write the klipper conf to the octoprint settings + data.pop("config", None) + + # save the rest of changed settings into config.yaml of octoprint + octoprint.plugin.SettingsPlugin.on_settings_save(self, data) + + def get_settings_restricted_paths(self): + return dict( + admin=[ + ["connection", "port"], + ["configuration", "configpath"], + ["configuration", "replace_connection_panel"] + ], + user=[ + ["macros"], + ["probe"] ] - ) - ) + ) - #-- Helpers - def sendMessage(self, type, subtype, title, payload): - self._plugin_manager.send_plugin_message( - self._identifier, - dict( - time=datetime.datetime.now().strftime("%H:%M:%S"), - type=type, - subtype=subtype, - title=title, - payload=payload - ) - ) + def get_settings_version(self): + return 2 - def pollStatus(self): - self._printer.commands("STATUS") + def on_settings_migrate(self, target, current): + if current is None: + settings = self._settings - def updateStatus(self, type, status): - self.sendMessage("status", type, status, status) + if settings.has(["serialport"]): + settings.set(["connection", "port"], + settings.get(["serialport"])) + settings.remove(["serialport"]) - def logInfo(self, message): - self.sendMessage("log", "info", message, message) + if settings.has(["replace_connection_panel"]): + settings.set( + ["connection", "replace_connection_panel"], + settings.get(["replace_connection_panel"]) + ) + settings.remove(["replace_connection_panel"]) - def logError(self, error): - self.sendMessage("log", "error", error, error) + if settings.has(["probeHeight"]): + settings.set(["probe", "height"], + settings.get(["probeHeight"])) + settings.remove(["probeHeight"]) + + if settings.has(["probeLift"]): + settings.set(["probe", "lift"], settings.get(["probeLift"])) + settings.remove(["probeLift"]) + + if settings.has(["probeSpeedXy"]): + settings.set(["probe", "speed_xy"], + settings.get(["probeSpeedXy"])) + settings.remove(["probeSpeedXy"]) + + if settings.has(["probeSpeedZ"]): + settings.set(["probe", "speed_z"], + settings.get(["probeSpeedZ"])) + settings.remove(["probeSpeedZ"]) + + if settings.has(["probePoints"]): + points = settings.get(["probePoints"]) + points_new = [] + for p in points: + points_new.append( + dict(name="", x=int(p["x"]), y=int(p["y"]), z=0)) + settings.set(["probe", "points"], points_new) + settings.remove(["probePoints"]) + + if settings.has(["configPath"]): + settings.set(["config_path"], settings.get(["configPath"])) + settings.remove(["configPath"]) + + # -- Template Plugin + + def get_template_configs(self): + return [ + dict(type="navbar", custom_bindings=True), + dict(type="settings", custom_bindings=True), + dict( + type="generic", + name="Assisted Bed Leveling", + template="klipper_leveling_dialog.jinja2", + custom_bindings=True + ), + dict( + type="generic", + name="PID Tuning", + template="klipper_pid_tuning_dialog.jinja2", + custom_bindings=True + ), + dict( + type="generic", + name="Coordinate Offset", + template="klipper_offset_dialog.jinja2", + custom_bindings=True + ), + dict( + type="tab", + name="Klipper", + template="klipper_tab_main.jinja2", + suffix="_main", + custom_bindings=True + ), + dict(type="sidebar", + custom_bindings=True, + icon="rocket", + replaces="connection" if self._settings.get_boolean( + ["connection", "replace_connection_panel"]) else "" + ), + dict( + type="generic", + name="Performance Graph", + template="klipper_graph_dialog.jinja2", + custom_bindings=True + ), + dict( + type="generic", + name="Macro Dialog", + template="klipper_param_macro_dialog.jinja2", + custom_bindings=True + ) + ] + + # -- Asset Plugin + + def get_assets(self): + return dict( + js=["js/klipper.js", + "js/klipper_settings.js", + "js/klipper_leveling.js", + "js/klipper_pid_tuning.js", + "js/klipper_offset.js", + "js/klipper_param_macro.js", + "js/klipper_graph.js" + ], + css=["css/klipper.css"], + less=["css/klipper.less"] + ) + + # -- Event Handler Plugin + + def on_event(self, event, payload): + if "UserLoggedIn" == event: + self.update_status("info", "Klipper: Standby") + if "Connecting" == event: + self.update_status("info", "Klipper: Connecting ...") + elif "Connected" == event: + self.update_status("info", "Klipper: Connected to host") + self.log_info( + "Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"])) + elif "Disconnected" == event: + self.update_status("info", "Klipper: Disconnected from host") + elif "Error" == event: + self.update_status("error", "Klipper: Error") + self.log_error(payload["error"]) + + # -- GCODE Hook + + def on_parse_gcode(self, comm, line, *args, **kwargs): + + if "FIRMWARE_VERSION" in line: + printerInfo = parse_firmware_line(line) + if "FIRMWARE_VERSION" in printerInfo: + self.log_info("Firmware version: {}".format( + printerInfo["FIRMWARE_VERSION"])) + elif "//" in line: + self._message = self._message + line.strip('/') + if not self._parsing_response: + self.update_status("info", self._message) + self._parsing_response = True + else: + if self._parsing_response: + self._parsing_response = False + self.log_info(self._message) + self._message = "" + if "!!" in line: + msg = line.strip('!') + self.update_status("error", msg) + self.log_error(msg) + return line + + def get_api_commands(self): + return dict( + listLogFiles=[], + getStats=["logFile"], + reloadConfig=[] + ) + + def on_api_command(self, command, data): + if command == "listLogFiles": + files = [] + logpath = os.path.expanduser( + self._settings.get(["configuration", "logpath"]) + ) + if self.file_exist(logpath): + for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"): + filesize = os.path.getsize(f) + files.append(dict( + name=os.path.basename( + f) + " ({:.1f} KB)".format(filesize / 1000.0), + file=f, + size=filesize + )) + return flask.jsonify(data=files) + else: + return flask.jsonify(data=files) + elif command == "getStats": + if "logFile" in data: + log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer( + data["logFile"]) + return flask.jsonify(log_analyzer.analyze()) + elif command == "reloadConfig": + data = octoprint.plugin.SettingsPlugin.on_settings_load(self) + + configpath = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + + try: + f = open(configpath, "r") + data["config"] = f.read() + f.close() + except IOError: + self._logger.error( + "Error: Klipper config file not found at: {}".format( + configpath) + ) + else: + self._settings.set(["config"], data["config"]) + self.send_message("reload", "config", "", data["config"]) + # send the configdata to frontend to update ace editor + return + + def get_update_information(self): + return dict( + klipper=dict( + displayName=self._plugin_name, + displayVersion=self._plugin_version, + type="github_release", + current=self._plugin_version, + user="thelastWallE", + repo="OctoprintKlipperPlugin", + pip="https://github.com/thelastWallE/OctoprintKlipperPlugin/archive/{target_version}.zip", + stable_branch=dict( + name="Stable", + branch="master", + comittish=["master"] + ), + prerelease_branches=[ + dict( + name="Release Candidate", + branch="rc", + comittish=["rc", "master"] + )] + ) + ) + + #-- Helpers + def send_message(self, type, subtype, title, payload): + self._plugin_manager.send_plugin_message( + self._identifier, + dict( + time=datetime.datetime.now().strftime("%H:%M:%S"), + type=type, + subtype=subtype, + title=title, + payload=payload + ) + ) + + def poll_status(self): + self._printer.commands("STATUS") + + def update_status(self, type, status): + self.send_message("status", type, status, status) + + def log_info(self, message): + self.send_message("log", "info", message, message) + + def log_console(self, consoletype, message): + # sends a message to frontend(in klipper.js -> self.onDataUpdaterPluginMessage) and write it to the console. + # _mtype, subtype=debug/info, title of message, message) + self.send_message("console", consoletype, message, message) + + def log_error(self, error): + self.send_message("log", "error", error, error) + + def file_exist(self, filepath): + if not os.path.isfile(filepath): + self.send_message("errorPopUp", "warning", "OctoKlipper Settings", + "Klipper " + filepath + " does not exist!") + return False + else: + return True + + def key_exist(self, dict, key1, key2): + try: + dict[key1][key2] + except KeyError: + return False + else: + return True + + def validate_configfile(self, dataToBeValidated): + """ + --->For now this just checks if the given data can be parsed<---- + + From https://www.opensourceforu.com/2015/03/practical-python-programming-writing-a-config-file-checker/ + + Validates a given Config File in filetobevalidated against a correct config file pointed to by goldenfilepath + returns a list of erroneous lines as a list[strings] + if config file is fine, it should return an empty list + """ + #len(ValidateFile('c:\example.cfg', 'c:\example.cfg' ))== 0 + + # learn golden file + #__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + #goldenfilepath = os.path.join(__location__, "goldenprinter.cfg") + #goldenconfig = ConfigParser.ConfigParser() + # dataToValidated.read(goldenfilepath) + + # learn file to be validated + + try: + dataToValidated = configparser.RawConfigParser() + dataToValidated.read_string(dataToBeValidated) + except dataToValidated.Error as error: + self._logger.error( + "Error: Invalid Klipper config file: {}".format(str(error)) + ) + self.send_message("errorPopUp", "warning", "OctoKlipper Settings", + "Invalid Klipper config file: " + str(error)) + return "False" + else: + return "True" + + #incorrectlines = [] + # for section in dataToValidated.sections(): + # #check each key is present in corresponding golden section + # for key in dataToValidated.options(section): + # if not goldenconfig.has_option(section,key): + # incorrectlines.append(key + "=" + dataToValidated.get(section,key)) + # # print incorrect lines + # if len(incorrectlines) > 0 : + # self.send_message("errorPopUp","warning", "OctoKlipper Settings", "Invalid Klipper config file: " + str(incorrectlines)) + # for k in incorrectlines: + # print k + # self._logger.error( + # "Error: Invalid Klipper config line: {}".format(str(k)) + # ) + # return incorrectlines - def checkFile(self,filetype,filepath): - if not os.path.isfile(filepath): - self.sendMessage("errorPopUp","warning", "OctoKlipper Settings", "Klipper " + filepath + " does not exist!") - - def keyexists(self, dict, key1, key2): - try: - dict[key1][key2] - except KeyError: - return False - else: - return True __plugin_name__ = "OctoKlipper" __plugin_pythoncompat__ = ">=2.7,<4" + def __plugin_load__(): - global __plugin_implementation__ - global __plugin_hooks__ - __plugin_implementation__ = KlipperPlugin() - __plugin_hooks__ = { - "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, - "octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode, - "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information - } + global __plugin_implementation__ + global __plugin_hooks__ + __plugin_implementation__ = KlipperPlugin() + __plugin_hooks__ = { + "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, + "octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode, + "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information + } diff --git a/octoprint_klipper/goldenprinter.cfg b/octoprint_klipper/goldenprinter.cfg new file mode 100644 index 0000000..9a47d96 --- /dev/null +++ b/octoprint_klipper/goldenprinter.cfg @@ -0,0 +1,519 @@ +# Golden printer.cfg to check against. This should contain all settings under each section that are available. + +[stepper_x] +step_pin: +# Step GPIO pin (triggered high). This parameter must be provided. +dir_pin: +# Direction GPIO pin (high indicates positive direction). This +# parameter must be provided. +enable_pin: +# Enable pin (default is enable high; use ! to indicate enable +# low). If this parameter is not provided then the stepper motor +# driver must always be enabled. +rotation_distance: +# Distance (in mm) that the axis travels with one full rotation of +# the stepper motor. This parameter must be provided. +microsteps: +# The number of microsteps the stepper motor driver uses. This +# parameter must be provided. +full_steps_per_rotation: 200 +# The number of full steps for one rotation of the stepper motor. +# Set this to 200 for a 1.8 degree stepper motor or set to 400 for a +# 0.9 degree motor. The default is 200. +gear_ratio: +# The gear ratio if the stepper motor is connected to the axis via a +# gearbox. For example, one may specify "5:1" if a 5 to 1 gearbox is +# in use. If the axis has multiple gearboxes one may specify a comma +# separated list of gear ratios (for example, "57:11, 2:1"). If a +# gear_ratio is specified then rotation_distance specifies the +# distance the axis travels for one full rotation of the final gear. +# The default is to not use a gear ratio. +endstop_pin: +# Endstop switch detection pin. This parameter must be provided for +# the X, Y, and Z steppers on cartesian style printers. +position_min: 0 +# Minimum valid distance (in mm) the user may command the stepper to +# move to. The default is 0mm. +position_endstop: +# Location of the endstop (in mm). This parameter must be provided +# for the X, Y, and Z steppers on cartesian style printers. +position_max: +# Maximum valid distance (in mm) the user may command the stepper to +# move to. This parameter must be provided for the X, Y, and Z +# steppers on cartesian style printers. +homing_speed: 5.0 +# Maximum velocity (in mm/s) of the stepper when homing. The default +# is 5mm/s. +homing_retract_dist: 5.0 +# Distance to backoff (in mm) before homing a second time during +# homing. Set this to zero to disable the second home. The default +# is 5mm. +homing_retract_speed: +# Speed to use on the retract move after homing in case this should +# be different from the homing speed, which is the default for this +# parameter +second_homing_speed: +# Velocity (in mm/s) of the stepper when performing the second home. +# The default is homing_speed/2. +homing_positive_dir: +# If true, homing will cause the stepper to move in a positive +# direction (away from zero); if false, home towards zero. It is +# better to use the default than to specify this parameter. The +# default is true if position_endstop is near position_max and false +# if near position_min. + +[tmc2209 stepper_x] +uart_pin: PB15 +run_current: 0.580 +hold_current: 0.500 +stealthchop_threshold: 250 + +[stepper_y] +step_pin: +dir_pin: +enable_pin: +rotation_distance: +microsteps: +full_steps_per_rotation: 200 +gear_ratio: +endstop_pin: +position_min: 0 +position_endstop: +position_max: +homing_speed: 5.0 +homing_retract_dist: 5.0 +homing_retract_speed: +second_homing_speed: +homing_positive_dir: + +[tmc2209 stepper_y] +uart_pin: PC6 +run_current: 0.580 +hold_current: 0.500 +stealthchop_threshold: 250 + +[stepper_z] +step_pin: +dir_pin: +enable_pin: +rotation_distance: +microsteps: +full_steps_per_rotation: 200 +gear_ratio: +endstop_pin: +position_min: 0 +position_endstop: +position_max: +homing_speed: 5.0 +homing_retract_dist: 5.0 +homing_retract_speed: +second_homing_speed: +homing_positive_dir: + +[tmc2209 stepper_z] +uart_pin: PC10 +run_current: 0.580 +hold_current: 0.500 +stealthchop_threshold: 10 + +[safe_z_home] +home_xy_position: 70,100 +speed: 50 +z_hop: 10 +z_hop_speed: 4 + +# --------------------For Delta Kinematics: -------------------------- +# The stepper_a section describes the stepper controlling the front +# left tower (at 210 degrees). This section also controls the homing +# parameters (homing_speed, homing_retract_dist) for all towers. +[stepper_a] +position_endstop: +# Distance (in mm) between the nozzle and the bed when the nozzle is +# in the center of the build area and the endstop triggers. This +# parameter must be provided for stepper_a; for stepper_b and +# stepper_c this parameter defaults to the value specified for +# stepper_a. +arm_length: +# Length (in mm) of the diagonal rod that connects this tower to the +# print head. This parameter must be provided for stepper_a; for +# stepper_b and stepper_c this parameter defaults to the value +# specified for stepper_a. +#angle: +# This option specifies the angle (in degrees) that the tower is +# at. The default is 210 for stepper_a, 330 for stepper_b, and 90 +# for stepper_c. + +# The stepper_b section describes the stepper controlling the front +# right tower (at 330 degrees). +[stepper_b] +position_endstop: +arm_length: + +# The stepper_c section describes the stepper controlling the rear +# tower (at 90 degrees). +[stepper_c] +position_endstop: +arm_length: + +# The delta_calibrate section enables a DELTA_CALIBRATE extended +# g-code command that can calibrate the tower endstop positions and +# angles. +[delta_calibrate] +radius: +# Radius (in mm) of the area that may be probed. This is the radius +# of nozzle coordinates to be probed; if using an automatic probe +# with an XY offset then choose a radius small enough so that the +# probe always fits over the bed. This parameter must be provided. +speed: 50 +# The speed (in mm/s) of non-probing moves during the calibration. +# The default is 50. +horizontal_move_z: 5 +# The height (in mm) that the head should be commanded to move to +# just prior to starting a probe operation. The default is 5. + +# -------------- For Polar Kinematics --------------------------- +# The stepper_bed section is used to describe the stepper controlling +# the bed. +[stepper_bed] +gear_ratio: +# A gear_ratio must be specified and rotation_distance may not be +# specified. For example, if the bed has an 80 toothed pulley driven +# by a stepper with a 16 toothed pulley then one would specify a +# gear ratio of "80:16". This parameter must be provided. +max_z_velocity: +# This sets the maximum velocity (in mm/s) of movement along the z +# axis. This setting can be used to restrict the maximum speed of +# the z stepper motor. The default is to use max_velocity for +# max_z_velocity. +max_z_accel: +# This sets the maximum acceleration (in mm/s^2) of movement along +# the z axis. It limits the acceleration of the z stepper motor. The +# default is to use max_accel for max_z_accel. + +# The stepper_arm section is used to describe the stepper controlling +# the carriage on the arm. +[stepper_arm] + +# --------------------- For + + +[extruder] +step_pin: PB3 +dir_pin: !PB4 +enable_pin: !PD2 +microsteps: 16 +rotation_distance: 33.400 +nozzle_diameter: 0.400 +filament_diameter: 1.750 +heater_pin: PC8 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PA0 +control: pid +pid_Kp=26.049 +pid_Ki=1.258 +pid_Kd=134.805 +min_temp: 0 +max_temp: 250 +pressure_advance: 0.07 +pressure_advance_smooth_time: 0.040 + +[tmc2209 extruder] +uart_pin: PC11 +run_current: 0.670 +hold_current: 0.500 +stealthchop_threshold: 5 + +[heater_bed] +heater_pin: PC9 +sensor_type: ATC Semitec 104GT-2 +sensor_pin: PC3 +control: pid +pid_Kp=72.740 +pid_Ki=1.569 +pid_Kd=842.877 +min_temp: 0 +max_temp: 130 + +[fan] +pin: PA8 + +[mcu] +serial: +# The serial port to connect to the MCU. If unsure (or if it +# changes) see the "Where's my serial port?" section of the FAQ. +# This parameter must be provided when using a serial port. +baud: 250000 +# The baud rate to use. The default is 250000. +canbus_uuid: +# If using a device connected to a CAN bus then this sets the unique +# chip identifier to connect to. This value must be provided when using +# CAN bus for communication. +canbus_interface: +# If using a device connected to a CAN bus then this sets the CAN +# network interface to use. The default is 'can0'. +pin_map: +# This option may be used to enable Arduino pin name aliases. The +# default is to not enable the aliases. +restart_method: +# This controls the mechanism the host will use to reset the +# micro-controller. The choices are 'arduino', 'cheetah', 'rpi_usb', +# and 'command'. The 'arduino' method (toggle DTR) is common on +# Arduino boards and clones. The 'cheetah' method is a special +# method needed for some Fysetc Cheetah boards. The 'rpi_usb' method +# is useful on Raspberry Pi boards with micro-controllers powered +# over USB - it briefly disables power to all USB ports to +# accomplish a micro-controller reset. The 'command' method involves +# sending a Klipper command to the micro-controller so that it can +# reset itself. The default is 'arduino' if the micro-controller +# communicates over a serial port, 'command' otherwise. + +[mcu my_extra_mcu] +serial: +baud: 250000 +canbus_uuid: +canbus_interface: +pin_map: +restart_method: + +[printer] +kinematics: +# The type of printer in use. This option may be one of: cartesian, +# corexy, corexz, delta, rotary_delta, polar, winch, or none. This +# parameter must be specified. +max_velocity: +# Maximum velocity (in mm/s) of the toolhead (relative to the +# print). This parameter must be specified. +max_accel: +# Maximum acceleration (in mm/s^2) of the toolhead (relative to the +# print). This parameter must be specified. +max_accel_to_decel: +# A pseudo acceleration (in mm/s^2) controlling how fast the +# toolhead may go from acceleration to deceleration. It is used to +# reduce the top speed of short zig-zag moves (and thus reduce +# printer vibration from these moves). The default is half of +# max_accel. +square_corner_velocity: 5.0 +# The maximum velocity (in mm/s) that the toolhead may travel a 90 +# degree corner at. A non-zero value can reduce changes in extruder +# flow rates by enabling instantaneous velocity changes of the +# toolhead during cornering. This value configures the internal +# centripetal velocity cornering algorithm; corners with angles +# larger than 90 degrees will have a higher cornering velocity while +# corners with angles less than 90 degrees will have a lower +# cornering velocity. If this is set to zero then the toolhead will +# decelerate to zero at each corner. The default is 5mm/s. +# +# -----delta Kinematics:------ +max_z_velocity: +# For delta printers this limits the maximum velocity (in mm/s) of +# moves with z axis movement. This setting can be used to reduce the +# maximum speed of up/down moves (which require a higher step rate +# than other moves on a delta printer). The default is to use +# max_velocity for max_z_velocity. +minimum_z_position: 0 +# The minimum Z position that the user may command the head to move +# to. The default is 0. +delta_radius: +# Radius (in mm) of the horizontal circle formed by the three linear +# axis towers. This parameter may also be calculated as: +# delta_radius = smooth_rod_offset - effector_offset - carriage_offset +# This parameter must be provided. +print_radius: +# The radius (in mm) of valid toolhead XY coordinates. One may use +# this setting to customize the range checking of toolhead moves. If +# a large value is specified here then it may be possible to command +# the toolhead into a collision with a tower. The default is to use +# delta_radius for print_radius (which would normally prevent a +# tower collision). + +max_z_velocity: 5 +max_z_accel: 100 +max_accel_to_decel: 1000 +square_corner_velocity: 5.0 + +[bed_screws] +screw1: 30,30 +screw2: 30,200 +screw3: 200,30 +screw4: 200,200 + +[bed_mesh] +speed: 50 +horizontal_move_z: 5 +mesh_min: 48,8 +mesh_max: 200,230 +probe_count: 7,7 +fade_start: 1.0 +fade_end: 0.0 +umber of +algorithm: bicubic + +[static_digital_output usb_pullup_enable] +pins: !PC13 + +[gcode_arcs] +resolution: 1.0 + +[bltouch] +sensor_pin: ^PC14 +control_pin: PA1 +x_offset: 48 +y_offset: -2 +z_offset: 1.23 +pin_move_time: 0.75 +speed: 1.0 +lift_speed: 3.0 +samples: 2 +sample_retract_dist: 3 + +[board_pins] +aliases: + # EXP1 header + EXP1_1=PB5, EXP1_3=PA9, EXP1_5=PA10, EXP1_7=PB8, EXP1_9=, + EXP1_2=PB6, EXP1_4=, EXP1_6=PB9, EXP1_8=PB7, EXP1_10=<5V> + +[display] +lcd_type: st7920 +cs_pin: EXP1_7 +sclk_pin: EXP1_6 +sid_pin: EXP1_8 +encoder_pins: ^EXP1_5, ^EXP1_3 +click_pin: ^!EXP1_2 + +[output_pin beeper] +pin: EXP1_1 + +[firmware_retraction] +retract_length: 2.7 +retract_speed: 25 +unretract_extra_length: 0.0 +unretract_speed: 25 + +[virtual_sdcard] +path: /home/pi/.octoprint/uploads + +[gcode_macro DO_MESH] +gcode: + M140 S60 + G28 + M190 S60 ; Heat Bed to 60C + BED_MESH_CALIBRATE ;Mesh leveling + M140 S0 ;Turn-off bed + SAVE_CONFIG + +[gcode_macro CHECK_DISTANCE] +gcode: + M140 S60 + G28 + M190 S60 ; Heat Bed to 60C + G1 Z15.8 F1000 + +[gcode_macro START_PRINT] +gcode: + M220 S100 ;Reset Feedrate + M221 S100 ;Reset Flowrate + G92 E0 ;Reset Extruder + G1 Z1.0 F3000 ;Move Z Axis up + G1 X0 Y20 Z0.3 F5000.0 ;Move to start position + G1 X0 Y200.0 Z0.3 F1500.0 E15 ;Draw the first line + G1 X0.4 Y200.0 Z0.3 F5000.0 ;Move to side a little + G1 X0.4 Y20 Z0.3 F1500.0 E27 ;Draw the second line + G1 Z1.6 F3000 ;Move Z Axis up + +[gcode_macro END_PRINT] +gcode: + ;MESH:ENDGCODE + G91 ;Relative positioning + G92 E0 ;Reset Extruder + G1 F2400 E-8 + G1 Z0.2 F2400 ;Raise Z + G1 X5 Y5 F3000 ;Wipe out + G1 Z10 ;Raise Z more + G90 ;Absolute positionning + G1 X0 Y{machine_depth} ;Present print + M106 S0 ;Turn-off fan + M140 S0 ;Turn-off bed + M84 X Y E ;Disable all steppers but Z + +[gcode_macro SET_RETRACTIONLENGTH] +gcode: + SET_RETRACTION RETRACT_LENGTH={params.LENGTH|float} + GET_RETRACTION + +[pause_resume] +# M600: Filament Change. This macro will pause the printer, move the +# tool to the change position, and retract the filament 50mm. Adjust +# the retraction settings for your own extruder. After filament has +# been changed, the print can be resumed from its previous position +# with the "RESUME" gcode. + +[gcode_macro PARK] +gcode: + G1 X125 Y200.0 Z200.0 F4000 + +[gcode_macro FILAMENT_LOAD] +gcode: + M83 ; set e to relative positioning + G92 E0.0 + G1 E70 F500 ; Initially go fast + G92 E0.0 + G1 E10 F200 ; then go slow + G92 E0.0 + +[gcode_macro FILAMENT_UNLOAD] +gcode: + M83 ; set e to relative positioning + # wiggle filament out of the nozzle + G1 E0.5 F1000 + G1 E-0.5 F1000 + G1 E1.0 F1000 + G1 E-1.0 F1000 + G1 E1.5 F1000 + G1 E-1.5 F1000 + G1 E2.0 F1000 + + G1 E-100.0 F3000 ;fully unload + G92 E0.0 + +[gcode_macro M600] +default_parameter_X: 0 +default_parameter_Y: 0 +default_parameter_Z: 50 +gcode: + SAVE_GCODE_STATE NAME=M600_state + PAUSE + G91 ;relative + G1 E-.2 F2700 + G1 Z{Z} ;raise nozzle + G90 ;absolute + G1 X{X} Y{Y} F3000 + G91 ;relative + M300 P1000 ;beep + FILAMENT_UNLOAD ; + M300 P3000 ;beep + G90 ;absolute + RESTORE_GCODE_STATE NAME=M600_state + +#*# <---------------------- SAVE_CONFIG ----------------------> +#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated. +#*# +#*# [bed_mesh default] +#*# version = 1 +#*# points = +#*# 0.087500, 0.080000, 0.068750, 0.047500, 0.075000, 0.078750, 0.085000 +#*# 0.068750, 0.065000, 0.055000, 0.028750, 0.052500, 0.055000, 0.057500 +#*# 0.092500, 0.091250, 0.082500, 0.048750, 0.070000, 0.063750, 0.066250 +#*# 0.116250, 0.132500, 0.122500, 0.090000, 0.105000, 0.100000, 0.092500 +#*# 0.128750, 0.135000, 0.130000, 0.086250, 0.096250, 0.076250, 0.076250 +#*# 0.112500, 0.126250, 0.128750, 0.107500, 0.122500, 0.127500, 0.125000 +#*# 0.120000, 0.133750, 0.141250, 0.097500, 0.125000, 0.106250, 0.100000 +#*# tension = 0.2 +#*# min_x = 48.0 +#*# algo = bicubic +#*# y_count = 7 +#*# mesh_y_pps = 2 +#*# min_y = 8.0 +#*# x_count = 7 +#*# max_y = 230.0 +#*# mesh_x_pps = 2 +#*# max_x = 199.97 diff --git a/octoprint_klipper/modules/KlipperLogAnalyzer.py b/octoprint_klipper/modules/KlipperLogAnalyzer.py index b7c971e..3f1881d 100644 --- a/octoprint_klipper/modules/KlipperLogAnalyzer.py +++ b/octoprint_klipper/modules/KlipperLogAnalyzer.py @@ -4,12 +4,12 @@ # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. - + # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. - + # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . @@ -27,12 +27,24 @@ class KlipperLogAnalyzer(): def __init__(self, log_file): self.log_file = log_file + def read_log_file(self, logname): + try: + f = open(logname, 'r') + logdata = f.read() + f.close() + except IOError: + print("Couldn't open log file") + return logdata + def analyze(self): data = self.parse_log(self.log_file, None) if not data: - result = dict(error= "No relevant data available in \"{}\"".format(self.log_file)) + result1 = dict(error= "No relevant data available in \"{}\"".format(self.log_file)) else: - result = self.plot_mcu(data, self.MAXBANDWIDTH) + result1 = self.plot_mcu(data, self.MAXBANDWIDTH) + result = dict(plot = result1, + logfiledata = self.read_log_file(self.log_file) + ) return result def parse_log(self, logname, mcu): diff --git a/octoprint_klipper/static/css/klipper.css b/octoprint_klipper/static/css/klipper.css index 3dd3026..e452020 100644 --- a/octoprint_klipper/static/css/klipper.css +++ b/octoprint_klipper/static/css/klipper.css @@ -112,10 +112,23 @@ ul#klipper-settings { padding-top: 2px; } +#klipper_graph_dialog { + width: 1050px; +} + +#klipper_graph_dialog .full-sized-box{ + width: 1000px; + margin: 0 auto; +} + #klipper_graph_dialog form { margin: 0; } +#klipper_graph_dialog select { + width: auto; +} + #klipper_graph_dialog .graph-footer { bottom:0; } @@ -133,8 +146,14 @@ ul#klipper-settings { #klipper_graph_dialog .fill-checkbox { display: block; position: absolute; - bottom: 5px; - margin-left: 10px; + top: 0%; + left: 50%; +} + +#klipper_graph_dialog .help-inline { + display: block; + position: absolute; + top: 0px; } #klipper_graph_canvas { diff --git a/octoprint_klipper/static/js/klipper.js b/octoprint_klipper/static/js/klipper.js index 08ca430..6f3c43e 100644 --- a/octoprint_klipper/static/js/klipper.js +++ b/octoprint_klipper/static/js/klipper.js @@ -16,6 +16,14 @@ $(function () { function KlipperViewModel(parameters) { var self = this; + var console_debug = false; + + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache" + }); + + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); self.settings = parameters[0]; self.loginState = parameters[1]; @@ -23,20 +31,24 @@ $(function () { self.levelingViewModel = parameters[3]; self.paramMacroViewModel = parameters[4]; self.access = parameters[5]; - + self.shortStatus = ko.observable(); self.logMessages = ko.observableArray(); self.showPopUp = function(popupType, popupTitle, message){ - var title = popupType.toUpperCase() + ": " + popupTitle; - new PNotify({ - title: title, - text: message, - type: popupType, - hide: false + var title = popupType.toUpperCase() + ": " + popupTitle; + new PNotify({ + title: title, + text: message, + type: popupType, + hide: false }); }; - + + self.onSettingsShown = function () { + self.reloadConfig(); + } + self.showLevelingDialog = function () { var dialog = $("#klipper_leveling_dialog"); dialog.modal({ @@ -118,20 +130,31 @@ $(function () { }; self.onDataUpdaterPluginMessage = function(plugin, data) { - if(plugin == "klipper") { - if ("warningPopUp" == data.type){ - self.showPopUp(data.subtype, data.title, data.payload); - return; - } - if ("errorPopUp" == data.type){ - self.showPopUp(data.subtype, data.title, data.payload); - return; - } - if(data.type == "status") { - self.shortStatus(data.payload); - } else { - self.logMessage(data.time, data.subtype, data.payload); - } + if(plugin == "klipper") { + if ("warningPopUp" == data.type){ + self.showPopUp(data.subtype, data.title, data.payload); + return; + } + + if ("errorPopUp" == data.type){ + self.showPopUp(data.subtype, data.title, data.payload); + return; + } + + if ("console" == data.type) { + self.consoleMessage(data.subtype, data.payload); + return; + } + + if ("reload" == data.type){ + return; + } + + if(data.type == "status") { + self.shortStatus(data.payload); + } else { + self.logMessage(data.time, data.subtype, data.payload); + } } }; @@ -143,12 +166,43 @@ $(function () { }); }; + self.reloadConfig = function() { + var settings = { + "crossDomain": true, + "url": self.apiUrl, + "method": "POST", + "headers": self.header, + "processData": false, + "dataType": "json", + "data": JSON.stringify({command: "reloadConfig"}) + } + + $.ajax(settings).done(function (response) { + self.consoleMessage( + "debug", + "Reloaded from Backend " + response); + }); + } + + self.consoleMessage = function (type, message) { + if (type == "info"){ + console.info("OctoKlipper : " + message); + } else { + if (console_debug){ + console.debug("OctoKlipper : " + message); + } else { + return + } + } + return + } + self.onClearLog = function () { self.logMessages.removeAll(); }; self.isActive = function () { - return self.connectionState.isOperational() && this.hasRight("CONFIG"); + return self.connectionState.isOperational(); }; self.hasRight = function (right_role, type) { @@ -167,8 +221,7 @@ $(function () { $("a#navbar_show_settings").click(); $("li#settings_plugin_klipper_link a").click(); if (profile_type) { - var query = - "#klipper-settings a[data-profile-type='" + profile_type + "']"; + var query = "#klipper-settings a[data-profile-type='" + profile_type + "']"; $(query).click(); } }; diff --git a/octoprint_klipper/static/js/klipper_graph.js b/octoprint_klipper/static/js/klipper_graph.js index 137ac78..bfff755 100644 --- a/octoprint_klipper/static/js/klipper_graph.js +++ b/octoprint_klipper/static/js/klipper_graph.js @@ -4,12 +4,12 @@ // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. - + // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. - + // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . @@ -18,16 +18,17 @@ $(function() { function KlipperGraphViewModel(parameters) { var self = this; self.loginState = parameters[0]; - + self.header = OctoPrint.getRequestHeaders({ "content-type": "application/json", "cache-control": "no-cache" }); - + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); - + self.availableLogFiles = ko.observableArray(); self.logFile = ko.observable(); + self.klippylogFile = ko.observable(); self.status = ko.observable(); self.datasets = ko.observableArray(); self.datasetFill = ko.observable(false); @@ -40,24 +41,24 @@ function KlipperGraphViewModel(parameters) { self.canvas = $("#klipper_graph_canvas")[0] self.canvasContext = self.canvas.getContext("2d"); self.spinnerDialog = $("#klipper_graph_spinner"); - + Chart.defaults.global.elements.line.borderWidth=1; Chart.defaults.global.elements.line.fill= false; Chart.defaults.global.elements.point.radius= 0; - + var myChart = new Chart(self.canvas, { type: "line" }); - + if(self.loginState.loggedIn()) { self.listLogFiles(); } } - + self.onUserLoggedIn = function(user) { self.listLogFiles(); } - + self.listLogFiles = function() { var settings = { "crossDomain": true, @@ -68,31 +69,31 @@ function KlipperGraphViewModel(parameters) { "dataType": "json", "data": JSON.stringify({command: "listLogFiles"}) } - + $.ajax(settings).done(function (response) { self.availableLogFiles.removeAll(); self.availableLogFiles(response["data"]); }); } - + self.saveGraphToPng = function() { button = $('#download-btn'); var dataURL = self.canvas.toDataURL("image/png");//.replace("image/png", "image/octet-stream"); button.attr("href", dataURL); } - + self.showSpinner = function(showDialog) { if (showDialog) { self.spinnerDialog.modal({ show: true, keyboard: false, - backdrop: "static" + backdrop: "static" }); } else { self.spinnerDialog.modal("hide"); } } - + self.toggleDatasetFill = function() { if(self.datasets) { for (i=0; i < self.datasets().length; i++) { @@ -102,11 +103,11 @@ function KlipperGraphViewModel(parameters) { } return true } - + self.convertTime = function(val) { return moment(val, "X"); } - + self.loadData = function() { var settings = { "crossDomain": true, @@ -122,17 +123,18 @@ function KlipperGraphViewModel(parameters) { } ) } - + self.showSpinner(true); - + $.ajax(settings).done(function (response) { self.status("") self.datasetFill(false); - + self.showSpinner(false); - - if("error" in response) { - self.status(response.error); + self.klippylogFile(response.logfiledata); + + if("error" in response.plot) { + self.status(response.plot.error); } else { self.datasets.removeAll(); self.datasets.push( @@ -141,40 +143,40 @@ function KlipperGraphViewModel(parameters) { backgroundColor: "rgba(199, 44, 59, 0.5)", borderColor: "rgb(199, 44, 59)", yAxisID: 'y-axis-1', - data: response.loads + data: response.plot.loads }); - + self.datasets.push( { label: "Bandwith", backgroundColor: "rgba(255, 130, 1, 0.5)", borderColor: "rgb(255, 130, 1)", yAxisID: 'y-axis-1', - data: response.bwdeltas + data: response.plot.bwdeltas }); - + self.datasets.push( { label: "Host Buffer", backgroundColor: "rgba(0, 145, 106, 0.5)", borderColor: "rgb(0, 145, 106)", yAxisID: 'y-axis-1', - data: response.buffers + data: response.plot.buffers }); - + self.datasets.push( { label: "Awake Time", backgroundColor: "rgba(33, 64, 95, 0.5)", borderColor: "rgb(33, 64, 95)", yAxisID: 'y-axis-1', - data: response.awake + data: response.plot.awake }); - + self.chart = new Chart(self.canvas, { type: "line", data: { - labels: response.times, + labels: response.plot.times, datasets: self.datasets() }, options: { @@ -211,7 +213,7 @@ function KlipperGraphViewModel(parameters) { ] }, legend: { - + } } }); diff --git a/octoprint_klipper/static/js/klipper_settings.js b/octoprint_klipper/static/js/klipper_settings.js index d47c927..0b4cbc3 100644 --- a/octoprint_klipper/static/js/klipper_settings.js +++ b/octoprint_klipper/static/js/klipper_settings.js @@ -4,12 +4,12 @@ // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. - + // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. - + // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . @@ -17,64 +17,130 @@ $(function() { $('#klipper-settings a:first').tab('show'); function KlipperSettingsViewModel(parameters) { var self = this; + var obKlipperConfig = null; + var editor = null; + self.settings = parameters[0]; + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache" + }); + + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.addMacro = function() { - self.settings.settings.plugins.klipper.macros.push({ - name: 'Macro', - macro: '', - sidebar: true, - tab: true - }); + self.settings.settings.plugins.klipper.macros.push({ + name: 'Macro', + macro: '', + sidebar: true, + tab: true + }); } - + self.removeMacro = function(macro) { - self.settings.settings.plugins.klipper.macros.remove(macro); + self.settings.settings.plugins.klipper.macros.remove(macro); } - + self.moveMacroUp = function(macro) { - self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro) + self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro) } - + self.moveMacroDown = function(macro) { - self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro) + self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro) } - + self.addProbePoint = function() { - self.settings.settings.plugins.klipper.probe.points.push( - { - name: 'point-#', - x:0, y:0, z:0 - } - ); + self.settings.settings.plugins.klipper.probe.points.push( + { + name: 'point-#', + x:0, y:0, z:0 + } + ); } - + self.removeProbePoint = function(point) { - self.settings.settings.plugins.klipper.probe.points.remove(point); + self.settings.settings.plugins.klipper.probe.points.remove(point); } - + self.moveProbePointUp = function(macro) { - self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro) + self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro) } - + self.moveProbePointDown = function(macro) { - self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro) + self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro) } - + self.moveItemDown = function(list, item) { - var i = list().indexOf(item); - if (i < list().length - 1) { - var rawList = list(); - list.splice(i, 2, rawList[i + 1], rawList[i]); - } + var i = list().indexOf(item); + if (i < list().length - 1) { + var rawList = list(); + list.splice(i, 2, rawList[i + 1], rawList[i]); + } } - + self.moveItemUp = function(list, item) { - var i = list().indexOf(item); - if (i > 0) { - var rawList = list(); - list.splice(i-1, 2, rawList[i], rawList[i-1]); - } + var i = list().indexOf(item); + if (i > 0) { + var rawList = list(); + list.splice(i-1, 2, rawList[i], rawList[i-1]); + } + } + + self.configBound = function (config) { + config.withSilence = function() { + this.notifySubscribers = function() { + if (!this.isSilent) { + ko.subscribable.fn.notifySubscribers.apply(this, arguments); + } + } + + this.silentUpdate = function(newValue) { + this.isSilent = true; + this(newValue); + this.isSilent = false; + }; + + return this; + } + + obKlipperConfig = config.withSilence(); + if (editor) { + editor.setValue(obKlipperConfig()); + editor.clearSelection(); + } + return obKlipperConfig; + } + + ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/"); + editor = ace.edit("plugin-klipper-config"); + editor.setTheme("ace/theme/monokai"); + editor.session.setMode("ace/mode/klipper_config"); + editor.setOptions({ + autoScrollEditorIntoView: true, + maxLines: "Infinity" + }) + + editor.session.on('change', function(delta) { + if (obKlipperConfig) { + obKlipperConfig.silentUpdate(editor.getValue()); + } + }); + + // Uncomment this if not using maxLines: "Infinity"... + // setInterval(function(){ editor.resize(); }, 500); + self.onDataUpdaterPluginMessage = function(plugin, data) { + if(plugin == "klipper") { + if ("reload" == data.type){ + if ("config" == data.subtype){ + if (editor.session) { + editor.session.setValue(data.payload); + editor.clearSelection(); + } + } + return + } + } } } diff --git a/octoprint_klipper/templates/klipper_graph_dialog.jinja2 b/octoprint_klipper/templates/klipper_graph_dialog.jinja2 index f0ea437..a0508e8 100644 --- a/octoprint_klipper/templates/klipper_graph_dialog.jinja2 +++ b/octoprint_klipper/templates/klipper_graph_dialog.jinja2 @@ -5,12 +5,12 @@
- +
- +
@@ -61,7 +61,7 @@
-
+
@@ -197,57 +197,11 @@ +
-
diff --git a/octoprint_klipper/templates/klipper_tab_main.jinja2 b/octoprint_klipper/templates/klipper_tab_main.jinja2 index aa92d21..acd9cb9 100644 --- a/octoprint_klipper/templates/klipper_tab_main.jinja2 +++ b/octoprint_klipper/templates/klipper_tab_main.jinja2 @@ -10,7 +10,7 @@
  - @@ -56,9 +56,9 @@ title="Sets a offset for subsequent GCODE coordinates."> {{ _('Coordinate Offset') }} - @@ -73,4 +73,4 @@ - \ No newline at end of file +