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 +