Merge branch 'feat/multipleCfg'

This commit is contained in:
thelastWallE 2021-09-05 15:28:57 +02:00
parent d70198b14b
commit b2575c55db
21 changed files with 2397 additions and 1176 deletions

View File

@ -9,7 +9,9 @@ charset = utf-8
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_style = space indent_style = space
max_line_length = 90 max_line_length = 180
indent_size = 2
[**.py] [**.py]
indent_size = 4 indent_size = 4

View File

@ -14,16 +14,22 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import datetime
import logging import logging
import octoprint.plugin import octoprint.plugin
import octoprint.plugin.core import octoprint.plugin.core
import glob import glob
import os import os
import time
import sys import sys
from octoprint.server import NO_CONTENT
from octoprint.util import is_hidden_path
from octoprint.util import get_formatted_size
from . import util, cfgUtils, logger
from octoprint.util.comm import parse_firmware_line from octoprint.util.comm import parse_firmware_line
from octoprint.access.permissions import Permissions, ADMIN_GROUP from octoprint.access.permissions import Permissions, ADMIN_GROUP
from .modules import KlipperLogAnalyzer from .modules import KlipperLogAnalyzer
from octoprint.server.util.flask import restricted_access
import flask import flask
from flask_babel import gettext from flask_babel import gettext
@ -35,13 +41,16 @@ except ImportError:
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
import StringIO import StringIO
MAX_UPLOAD_SIZE = 5 * 1024 * 1024 # 5Mb
class KlipperPlugin( class KlipperPlugin(
octoprint.plugin.StartupPlugin, octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin, octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin, octoprint.plugin.SettingsPlugin,
octoprint.plugin.AssetPlugin, octoprint.plugin.AssetPlugin,
octoprint.plugin.SimpleApiPlugin, octoprint.plugin.SimpleApiPlugin,
octoprint.plugin.EventHandlerPlugin): octoprint.plugin.EventHandlerPlugin,
octoprint.plugin.BlueprintPlugin):
_parsing_response = False _parsing_response = False
_parsing_check_response = True _parsing_check_response = True
@ -74,8 +83,10 @@ class KlipperPlugin(
self._settings.global_set( self._settings.global_set(
["serial", "additionalPorts"], additional_ports) ["serial", "additionalPorts"], additional_ports)
self._settings.save() self._settings.save()
self.log_info( logger.log_info(
"Added klipper serial port {} to list of additional ports.".format(klipper_port)) self,
"Added klipper serial port {} to list of additional ports.".format(klipper_port)
)
# -- Settings Plugin # -- Settings Plugin
@ -126,6 +137,7 @@ class KlipperPlugin(
debug_logging=False, debug_logging=False,
configpath="~/printer.cfg", configpath="~/printer.cfg",
old_config="", old_config="",
temp_config="",
logpath="/tmp/klippy.log", logpath="/tmp/klippy.log",
reload_command="RESTART", reload_command="RESTART",
shortStatus_navbar=True, shortStatus_navbar=True,
@ -141,71 +153,32 @@ class KlipperPlugin(
configpath = os.path.expanduser( configpath = os.path.expanduser(
self._settings.get(["configuration", "configpath"]) self._settings.get(["configuration", "configpath"])
) )
data["config"] = "" standardconfigfile = os.path.join(configpath, "printer.cfg")
try: data["config"] = cfgUtils.get_cfg(self, standardconfigfile)
f = open(configpath, "r") util.send_message(self, "reload", "config", "", data["config"])
data["config"] = f.read()
f.close()
except IOError:
self.log_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 # send the configdata to frontend to update ace editor
return data return data
def on_settings_save(self, data): def on_settings_save(self, data):
self.log_debug(
"Save klipper configs"
)
if "config" in data: if "config" in data:
check_parse = self._settings.get(["configuration", "parse_check"]) if cfgUtils.save_cfg(self, data["config"], "printer.cfg"):
self.log_debug("check_parse: {}".format(check_parse)) #load the reload command from changed data if it is not existing load the saved setting
if util.key_exist(data, "configuration", "reload_command"):
if sys.version_info[0] < 3: reload_command = os.path.expanduser(
data["config"] = data["config"].encode('utf-8') data["configuration"]["reload_command"]
)
# 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):
self.copy_cfg_to_backup(configpath)
if self._parsing_check_response or not check_parse:
try:
f = open(configpath, "w")
f.write(data["config"])
f.close()
self.log_debug("Writing Klipper config to {}".format(configpath))
except IOError:
self.log_error("Error: Couldn't write Klipper config file: {}".format(configpath))
else: else:
#load the reload command from changed data if it is not existing load the saved setting reload_command = self._settings.get(["configuration", "reload_command"])
if self.key_exist(data, "configuration", "reload_command"):
reload_command = os.path.expanduser(
data["configuration"]["reload_command"]
)
else:
reload_command = self._settings.get(["configuration", "reload_command"])
if reload_command != "manually": if reload_command != "manually":
# Restart klippy to reload config # Restart klippy to reload config
self._printer.commands(reload_command) self._printer.commands(reload_command)
self.log_info("Restarting Klipper.") logger.log_info(self, "Restarting Klipper.")
# we dont want to write the klipper conf to the octoprint settings # we dont want to write the klipper conf to the octoprint settings
data.pop("config", None) else:
# save not sure. saving to the octoprintconfig:
self._settings.set(["configuration", "temp_config"], data)
data.pop("config", None)
# save the rest of changed settings into config.yaml of octoprint # save the rest of changed settings into config.yaml of octoprint
old_debug_logging = self._settings.get_boolean(["configuration", "debug_logging"]) old_debug_logging = self._settings.get_boolean(["configuration", "debug_logging"])
@ -280,14 +253,14 @@ class KlipperPlugin(
settings.remove(["probePoints"]) settings.remove(["probePoints"])
if settings.has(["configPath"]): if settings.has(["configPath"]):
self.log_info("migrate setting for: configPath") logger.log_info(self, "migrate setting for: configPath")
settings.set(["config_path"], settings.get(["configPath"])) settings.set(["config_path"], settings.get(["configPath"]))
settings.remove(["configPath"]) settings.remove(["configPath"])
if current is not None and current < 3: if current is not None and current < 3:
settings = self._settings settings = self._settings
if settings.has(["configuration", "navbar"]): if settings.has(["configuration", "navbar"]):
self.log_info("migrate setting for: configuration/navbar") logger.log_info(self, "migrate setting for: configuration/navbar")
settings.set(["configuration", "shortStatus_navbar"], settings.get(["configuration", "navbar"])) settings.set(["configuration", "shortStatus_navbar"], settings.get(["configuration", "navbar"]))
settings.remove(["configuration", "navbar"]) settings.remove(["configuration", "navbar"])
@ -323,17 +296,29 @@ class KlipperPlugin(
custom_bindings=True custom_bindings=True
), ),
dict(type="sidebar", dict(type="sidebar",
custom_bindings=True, custom_bindings=True,
icon="rocket", icon="rocket",
replaces="connection" if self._settings.get_boolean( replaces="connection" if self._settings.get_boolean(
["connection", "replace_connection_panel"]) else "" ["connection", "replace_connection_panel"]) else ""
), ),
dict( dict(
type="generic", type="generic",
name="Performance Graph", name="Performance Graph",
template="klipper_graph_dialog.jinja2", template="klipper_graph_dialog.jinja2",
custom_bindings=True custom_bindings=True
), ),
dict(
type="generic",
name="Config Backups",
template="klipper_backups_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="Config Editor",
template="klipper_editor.jinja2",
custom_bindings=True
),
dict( dict(
type="generic", type="generic",
name="Macro Dialog", name="Macro Dialog",
@ -342,6 +327,12 @@ class KlipperPlugin(
) )
] ]
def get_template_vars(self):
return {
"max_upload_size": MAX_UPLOAD_SIZE,
"max_upload_size_str": get_formatted_size(MAX_UPLOAD_SIZE),
}
# -- Asset Plugin # -- Asset Plugin
def get_assets(self): def get_assets(self):
@ -352,8 +343,11 @@ class KlipperPlugin(
"js/klipper_pid_tuning.js", "js/klipper_pid_tuning.js",
"js/klipper_offset.js", "js/klipper_offset.js",
"js/klipper_param_macro.js", "js/klipper_param_macro.js",
"js/klipper_graph.js" "js/klipper_graph.js",
], "js/klipper_backup.js",
"js/klipper_editor.js"
],
clientjs=["clientjs/klipper.js"],
css=["css/klipper.css"] css=["css/klipper.css"]
) )
@ -361,18 +355,19 @@ class KlipperPlugin(
def on_event(self, event, payload): def on_event(self, event, payload):
if "UserLoggedIn" == event: if "UserLoggedIn" == event:
self.update_status("info", "Klipper: Standby") util.update_status(self, "info", "Klipper: Standby")
if "Connecting" == event: if "Connecting" == event:
self.update_status("info", "Klipper: Connecting ...") util.update_status(self, "info", "Klipper: Connecting ...")
elif "Connected" == event: elif "Connected" == event:
self.update_status("info", "Klipper: Connected to host") util.update_status(self, "info", "Klipper: Connected to host")
self.log_info( logger.log_info(
self,
"Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"])) "Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"]))
elif "Disconnected" == event: elif "Disconnected" == event:
self.update_status("info", "Klipper: Disconnected from host") util.update_status(self, "info", "Klipper: Disconnected from host")
elif "Error" == event: elif "Error" == event:
self.update_status("error", "Klipper: Error") util.update_status(self, "error", "Klipper: Error")
self.log_error(payload["error"]) logger.log_error(self, payload["error"])
# -- GCODE Hook # -- GCODE Hook
@ -381,22 +376,22 @@ class KlipperPlugin(
if "FIRMWARE_VERSION" in line: if "FIRMWARE_VERSION" in line:
printerInfo = parse_firmware_line(line) printerInfo = parse_firmware_line(line)
if "FIRMWARE_VERSION" in printerInfo: if "FIRMWARE_VERSION" in printerInfo:
self.log_info("Firmware version: {}".format( logger.log_info(self, "Firmware version: {}".format(
printerInfo["FIRMWARE_VERSION"])) printerInfo["FIRMWARE_VERSION"]))
elif "// probe" in line or "// Failed to verify BLTouch" in line: elif "// probe" in line or "// Failed to verify BLTouch" in line:
msg = line.strip('/') msg = line.strip('/')
self.log_info(msg) logger.log_info(self, msg)
self.write_parsing_response_buffer() self.write_parsing_response_buffer()
elif "//" in line: elif "//" in line:
# add lines with // to a buffer # add lines with // to a buffer
self._message = self._message + line.strip('/') self._message = self._message + line.strip('/')
if not self._parsing_response: if not self._parsing_response:
self.update_status("info", self._message) util.update_status(self, "info", self._message)
self._parsing_response = True self._parsing_response = True
elif "!!" in line: elif "!!" in line:
msg = line.strip('!') msg = line.strip('!')
self.update_status("error", msg) util.update_status(self, "error", msg)
self.log_error(msg) logger.log_error(self, msg)
self.write_parsing_response_buffer() self.write_parsing_response_buffer()
else: else:
self.write_parsing_response_buffer() self.write_parsing_response_buffer()
@ -406,7 +401,7 @@ class KlipperPlugin(
# write buffer with // lines after a gcode response without // # write buffer with // lines after a gcode response without //
if self._parsing_response: if self._parsing_response:
self._parsing_response = False self._parsing_response = False
self.log_info(self._message) logger.log_info(self, self._message)
self._message = "" self._message = ""
def get_api_commands(self): def get_api_commands(self):
@ -414,7 +409,6 @@ class KlipperPlugin(
listLogFiles=[], listLogFiles=[],
getStats=["logFile"], getStats=["logFile"],
reloadConfig=[], reloadConfig=[],
reloadCfgBackup=[],
checkConfig=["config"] checkConfig=["config"]
) )
@ -424,12 +418,12 @@ class KlipperPlugin(
logpath = os.path.expanduser( logpath = os.path.expanduser(
self._settings.get(["configuration", "logpath"]) self._settings.get(["configuration", "logpath"])
) )
if self.file_exist(logpath): if util.file_exist(self, logpath):
for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"): for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"):
filesize = os.path.getsize(f) filesize = os.path.getsize(f)
filemdate = time.strftime("%d.%m.%Y %H:%M",time.localtime(os.path.getctime(f)))
files.append(dict( files.append(dict(
name=os.path.basename( name=os.path.basename(f) + " (" + filemdate + ")",
f) + " ({:.1f} KB)".format(filesize / 1000.0),
file=f, file=f,
size=filesize size=filesize
)) ))
@ -441,30 +435,151 @@ class KlipperPlugin(
log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer( log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer(
data["logFile"]) data["logFile"])
return flask.jsonify(log_analyzer.analyze()) return flask.jsonify(log_analyzer.analyze())
elif command == "reloadConfig":
self.log_debug("reloadConfig") def is_blueprint_protected(self):
return self.reload_cfg() return False
elif command == "reloadCfgBackup":
self.log_debug("reloadCfgBackup") def route_hook(self, server_routes, *args, **kwargs):
configpath = os.path.expanduser( from octoprint.server.util.tornado import LargeResponseHandler, path_validation_factory
self._settings.get(["configuration", "configpath"]) from octoprint.util import is_hidden_path
) configpath = os.path.expanduser(
return self.copy_cfg_from_backup(configpath) self._settings.get(["configuration", "configpath"])
elif command == "checkConfig": )
if "config" in data: bak_path = os.path.join(self.get_plugin_data_folder(), "configs", "")
#self.write_cfg_backup(data["config"])
if self.key_exist(data, "configuration", "parse_check"): return [
check_parse = data["configuration"]["parse_check"] (r"/download/(.*)", LargeResponseHandler, dict(path=configpath,
else: as_attachment=True,
check_parse = self._settings.get(["configuration", "parse_check"]) path_validation=path_validation_factory(lambda path: not is_hidden_path(path),
if check_parse and not self.validate_configfile(data["config"]): status_code=404))),
self.log_debug("validateConfig not ok") (r"/download/backup(.*)", LargeResponseHandler, dict(path=bak_path,
self._settings.set(["configuration", "old_config"], data["config"]) as_attachment=True,
return flask.jsonify(checkConfig="not OK") path_validation=path_validation_factory(lambda path: not is_hidden_path(path),
else: status_code=404)))
self.log_debug("validateConfig ok") ]
self._settings.set(["configuration", "old_config"], "")
return flask.jsonify(checkConfig="OK") # API for Backups
# Get Content of a Backupconfig
@octoprint.plugin.BlueprintPlugin.route("/backup/<filename>", methods=["GET"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def get_backup(self, filename):
data_folder = self.get_plugin_data_folder()
full_path = os.path.realpath(os.path.join(data_folder, "configs", filename))
response = cfgUtils.get_cfg(self, full_path)
return flask.jsonify(response = response)
# Delete a Backupconfig
@octoprint.plugin.BlueprintPlugin.route("/backup/<filename>", methods=["DELETE"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def delete_backup(self, filename):
data_folder = self.get_plugin_data_folder()
full_path = os.path.realpath(os.path.join(data_folder, "configs", filename))
if (
full_path.startswith(data_folder)
and os.path.exists(full_path)
and not is_hidden_path(full_path)
):
try:
os.remove(full_path)
except Exception:
self._octoklipper_logger.exception("Could not delete {}".format(filename))
raise
return NO_CONTENT
# Get a list of all backuped configfiles
@octoprint.plugin.BlueprintPlugin.route("/backup/list", methods=["GET"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def list_backups(self):
files = cfgUtils.list_cfg_files(self, "backup")
return flask.jsonify(files = files)
# restore a backuped configfile
@octoprint.plugin.BlueprintPlugin.route("/backup/restore/<filename>", methods=["GET"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def restore_backup(self, filename):
configpath = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
data_folder = self.get_plugin_data_folder()
backupfile = os.path.realpath(os.path.join(data_folder, "configs", filename))
return flask.jsonify(restored = cfgUtils.copy_cfg(self, backupfile, configpath))
# API for Configs
# Get Content of a Configfile
@octoprint.plugin.BlueprintPlugin.route("/config/<filename>", methods=["GET"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def get_config(self, filename):
cfg_path = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
full_path = os.path.realpath(os.path.join(cfg_path, filename))
response = cfgUtils.get_cfg(self, full_path)
return flask.jsonify(response = response)
# Delete a Configfile
@octoprint.plugin.BlueprintPlugin.route("/config/<filename>", methods=["DELETE"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def delete_config(self, filename):
cfg_path = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
full_path = os.path.realpath(os.path.join(cfg_path, filename))
if (
full_path.startswith(cfg_path)
and os.path.exists(full_path)
and not is_hidden_path(full_path)
):
try:
os.remove(full_path)
except Exception:
self._octoklipper_logger.exception("Could not delete {}".format(filename))
raise
return NO_CONTENT
# Get a list of all configfiles
@octoprint.plugin.BlueprintPlugin.route("/config/list", methods=["GET"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def list_configs(self):
files = cfgUtils.list_cfg_files(self, "")
return flask.jsonify(files = files, max_upload_size = MAX_UPLOAD_SIZE)
# check syntax of a given data
@octoprint.plugin.BlueprintPlugin.route("/config/check", methods=["POST"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def check_config(self):
data = flask.request.json
data_to_check = data.get("DataToCheck", [])
response = cfgUtils.check_cfg(self, data_to_check)
return flask.jsonify(is_syntax_ok = response)
# save a configfile
@octoprint.plugin.BlueprintPlugin.route("/config/save", methods=["POST"])
@restricted_access
@Permissions.PLUGIN_KLIPPER_CONFIG.require(403)
def save_config(self):
data = flask.request.json
filename = data.get("filename", [])
Filecontent = data.get("DataToSave", [])
if filename == []:
flask.abort(
400,
description="Invalid request, the filename is not set",
)
saved = cfgUtils.save_cfg(self, Filecontent, filename)
if saved == True:
util.send_message(self, "reload", "configlist", "", "")
return flask.jsonify(saved = saved)
# APIs end
def get_update_information(self): def get_update_information(self):
return dict( return dict(
@ -490,193 +605,6 @@ class KlipperPlugin(
) )
) )
#-- Helpers
def send_message(self, type, subtype, title, payload):
"""
Send Message over API to FrontEnd
"""
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._octoklipper_logger.info(message)
self.send_message("log", "info", message, message)
def log_debug(self, message):
self._octoklipper_logger.debug(message)
self._logger.info(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", "debug", message, message)
def log_error(self, error):
self._octoklipper_logger.error(error)
self._logger.info(error)
self.send_message("log", "error", error, error)
def file_exist(self, filepath):
if not os.path.isfile(filepath):
self.send_message("PopUp", "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):
"""
--->SyntaxCheck for a given data<----
"""
check_parse = self._settings.get(["configuration", "parse_check"])
if check_parse:
try:
dataToValidated = configparser.RawConfigParser(strict=False)
#
if sys.version_info[0] < 3:
buf = StringIO.StringIO(dataToBeValidated)
dataToValidated.readfp(buf)
else:
dataToValidated.read_string(dataToBeValidated)
sections_search_list = ["bltouch",
"probe"]
value_search_list = [ "x_offset",
"y_offset",
"z_offset"]
try:
# cycle through sections and then values
for y in sections_search_list:
for x in value_search_list:
if dataToValidated.has_option(y, x):
a_float = dataToValidated.getfloat(y, x)
except ValueError as error:
self.log_error(
"Error: Invalid Value for <b>"+x+"</b> in Section: <b>"+y+"</b>\n" +
"{}".format(str(error))
)
self.send_message("PopUp", "warning", "OctoKlipper: Invalid Config\n",
"Config got not saved!\n" +
"You can reload your last changes\n" +
"on the 'Klipper Configuration' tab.\n\n" +
"Invalid Value for <b>"+x+"</b> in Section: <b>"+y+"</b>\n" + "{}".format(str(error)))
self._parsing_check_response = False
return False
except configparser.Error as error:
if sys.version_info[0] < 3:
error.message = error.message.replace("\\n","")
error.message = error.message.replace("file: u","Klipper Configuration", 1)
error.message = error.message.replace("'","", 2)
error.message = error.message.replace("u'","'", 1)
else:
error.message = error.message.replace("\\n","")
error.message = error.message.replace("file:","Klipper Configuration", 1)
error.message = error.message.replace("'","", 2)
self.log_error(
"Error: Invalid Klipper config file:\n" +
"{}".format(str(error))
)
self.send_message("PopUp", "warning", "OctoKlipper: Invalid Config data\n",
"Config got not saved!\n" +
"You can reload your last changes\n" +
"on the 'Klipper Configuration' tab.\n\n" + str(error))
self._parsing_check_response = False
return False
else:
self._parsing_check_response = True
return True
def reload_cfg(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.log_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
if sys.version_info[0] < 3:
data["config"] = data["config"].decode('utf-8')
return flask.jsonify(data=data["config"])
def copy_cfg_from_backup(self, dst):
"""
Copy the backuped config files in the data folder of OctoKlipper to the given destination
"""
from shutil import copy
bak_config_path = os.path.join(self.get_plugin_data_folder(), "configs/")
src_files = os.listdir(bak_config_path)
nio_files = []
self.log_debug("reloadCfgBackupPath:" + src_files)
for file_name in src_files:
full_file_name = os.path.join(bak_config_path, file_name)
if os.path.isfile(full_file_name):
try:
copy(full_file_name, dst)
except IOError:
self.log_error(
"Error: Klipper config file not found at: {}".format(
full_file_name)
)
nio_files.append(full_file_name)
else:
self.log_debug("File done: " + full_file_name)
return nio_files
def copy_cfg_to_backup(self, src):
"""
Copy the config file into the data folder of OctoKlipper
"""
from shutil import copyfile
filename = os.path.basename(src)
dst = os.path.join(self.get_plugin_data_folder(), "configs", "", filename)
self.log_debug("CopyCfgBackupPath:" + dst)
try:
copyfile(src, dst)
except IOError:
self.log_error(
"Error: Couldn't copy Klipper config file to {}".format(
dst)
)
else:
self.log_debug("CfgBackup writen")
__plugin_name__ = "OctoKlipper" __plugin_name__ = "OctoKlipper"
__plugin_pythoncompat__ = ">=2.7,<4" __plugin_pythoncompat__ = ">=2.7,<4"
__plugin_settings_overlay__ = { __plugin_settings_overlay__ = {
@ -690,12 +618,12 @@ __plugin_settings_overlay__ = {
} }
} }
def __plugin_load__(): def __plugin_load__():
global __plugin_implementation__ global __plugin_implementation__
global __plugin_hooks__ global __plugin_hooks__
__plugin_implementation__ = KlipperPlugin() __plugin_implementation__ = KlipperPlugin()
__plugin_hooks__ = { __plugin_hooks__ = {
"octoprint.server.http.routes": __plugin_implementation__.route_hook,
"octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions,
"octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode, "octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode,
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information

View File

@ -0,0 +1,285 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import glob
import os, time, sys
from . import util, logger
from octoprint.util import is_hidden_path
import flask
from flask_babel import gettext
def list_cfg_files(self, path: str) -> list:
"""Generate list of config files.
Args:
path (str): Path to the config files.
Returns:
list: for every file a dict with keys for name, file, size, mdate, url.
"""
files = []
if path=="backup":
cfg_path = os.path.join(self.get_plugin_data_folder(), "configs", "*")
else:
cfg_path = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
cfg_path = os.path.join(cfg_path, "*.cfg")
cfg_files = glob.glob(cfg_path)
logger.log_debug(self, "list_cfg_files Path: " + cfg_path)
f_counter = 1
for f in cfg_files:
filesize = os.path.getsize(f)
filemdate = time.localtime(os.path.getmtime(f))
files.append(dict(
name=os.path.basename(f),
file=f,
size=" ({:.1f} KB)".format(filesize / 1000.0),
mdate=time.strftime("%d.%m.%Y %H:%M", filemdate),
url= flask.url_for("index")
+ "plugin/klipper/download/"
+ os.path.basename(f),
))
logger.log_debug(self, "list_cfg_files " + str(f_counter) + ": " + f)
f_counter += 1
return files
def get_cfg(self, file):
"""Get the content of a configuration file.
Args:
file (str): The name of the file to read
Returns:
dict:
config (str): The configuration of the file
text (str): The text of the error
"""
response = {"config":"",
"text": ""}
if not file:
cfg_path = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
file = os.path.join(cfg_path, "printer.cfg")
if util.file_exist(self, file):
logger.log_debug(self, "get_cfg_files Path: " + file)
try:
with open(file, "r") as f:
response['config'] = f.read()
except IOError as Err:
logger.log_error(
self,
"Error: Klipper config file not found at: {}".format(file)
+ "\n IOError: {}".format(Err)
)
response['text'] = Err
return response
else:
if sys.version_info[0] < 3:
response['config'] = response.config.decode('utf-8')
return response
finally:
f.close()
else:
response['text'] = gettext("File not found!")
return response
def save_cfg(self, content, filename="printer.cfg"):
"""Save the configuration file to given file.
Args:
content (str): The content of the configuration.
filename (str): The filename of the configuration file. Default is "printer.cfg"
Returns:
bool: True if the configuration file was saved successfully. Otherwise False
"""
logger.log_debug(
self,
"Save klipper config"
)
if sys.version_info[0] < 3:
content = content.encode('utf-8')
check_parse = self._settings.get(["configuration", "parse_check"])
logger.log_debug(self, "check_parse on filesave: {}".format(check_parse))
configpath = os.path.expanduser(self._settings.get(["configuration", "configpath"]))
filepath = os.path.join(configpath, filename)
logger.log_debug(self, "save filepath: {}".format(filepath))
self._settings.set(["configuration", "temp_config"], content)
check = True
if check_parse:
check=check_cfg(self, content)
if check == True:
try:
logger.log_debug(self, "Writing Klipper config to {}".format(filepath))
with open(filepath, "w") as f:
f.write(content)
except IOError:
logger.log_error(self, "Error: Couldn't open Klipper config file: {}".format(filepath))
return False
else:
logger.log_debug(self, "Writen Klipper config to {}".format(filepath))
return True
finally:
f.close()
copy_cfg_to_backup(self, filepath)
else:
return False
def check_cfg(self, data):
"""Checks the given data on parsing errors.
Args:
data (str): Content to be validated.
Returns:
bool: True if the data is valid. False if it is not.
"""
try:
import configparser
except ImportError:
import ConfigParser as configparser
try:
dataToValidated = configparser.RawConfigParser(strict=False)
if sys.version_info[0] < 3:
import StringIO
buf = StringIO.StringIO(data)
dataToValidated.readfp(buf)
else:
dataToValidated.read_string(data)
sections_search_list = ["bltouch",
"probe"]
value_search_list = [ "x_offset",
"y_offset",
"z_offset"]
try:
# cycle through sections and then values
for y in sections_search_list:
for x in value_search_list:
if dataToValidated.has_option(y, x):
a_float = dataToValidated.getfloat(y, x)
if a_float:
pass
except ValueError as error:
logger.log_error(
self,
"Error: Invalid Value for <b>"+x+"</b> in Section: <b>"+y+"</b>\n"
+ "{}".format(str(error))
)
util.send_message(
self,
"PopUp",
"warning",
"OctoKlipper: Invalid Config\n",
"Config got not saved!\n\n"
+ "Invalid Value for <b>"+x+"</b> in Section: <b>"+y+"</b>\n"
+ "{}".format(str(error))
)
return False
except configparser.Error as error:
if sys.version_info[0] < 3:
error.message = error.message.replace("\\n","")
error.message = error.message.replace("file: u","Klipper Configuration", 1)
error.message = error.message.replace("'","", 2)
error.message = error.message.replace("u'","'", 1)
else:
error.message = error.message.replace("\\n","")
error.message = error.message.replace("file:","Klipper Configuration", 1)
error.message = error.message.replace("'","", 2)
logger.log_error(
self,
"Error: Invalid Klipper config file:\n"
+ "{}".format(str(error))
)
util.send_message(self, "PopUp", "warning", "OctoKlipper: Invalid Config data\n",
"Config got not saved!\n\n"
+ str(error))
return False
else:
return True
def copy_cfg(self, file, dst):
"""Copy the config file to the destination.
Args:
file (str): Filepath of the config file to copy.
dst (str): Path to copy the config file to.
Returns:
bool: True if the copy succeeded, False otherwise.
"""
from shutil import copy
if os.path.isfile(file):
try:
copy(file, dst)
except IOError:
logger.log_error(
self,
"Error: Klipper config file not found at: {}".format(file)
)
return False
else:
logger.log_debug(
self,
"File copied: "
+ file
)
return True
return False
def copy_cfg_to_backup(self, src):
"""Copy the config file to backup directory of OctoKlipper.
Args:
src (str): Path to the config file to copy.
Returns:
bool: True if the config file was copied successfully. False otherwise.
"""
from shutil import copyfile
if os.path.isfile(src):
cfg_path = os.path.join(self.get_plugin_data_folder(), "configs", "")
filename = os.path.basename(src)
if not os.path.exists(cfg_path):
try:
os.mkdir(cfg_path)
except OSError:
logger.log_error(self, "Error: Creation of the backup directory {} failed".format(cfg_path))
return False
else:
logger.log_debug(self, "Directory {} created".format(cfg_path))
dst = os.path.join(cfg_path, filename)
logger.log_debug(self, "copy_cfg_to_backup:" + src + " to " + dst)
if not src == dst:
try:
copyfile(src, dst)
except IOError:
logger.log_error(
self,
"Error: Couldn't copy Klipper config file to {}".format(dst)
)
return False
else:
logger.log_debug(self, "CfgBackup " + dst + " writen")
return True
else:
return False
else:
return False

View File

@ -0,0 +1,17 @@
from . import util
def log_info(self, message):
self._octoklipper_logger.info(message)
util.send_message(self, "log", "info", message, message)
def log_debug(self, message):
self._octoklipper_logger.debug(message)
self._logger.info(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)
util.send_message(self, "console", "debug", message, message)
def log_error(self, error):
self._octoklipper_logger.error(error)
self._logger.error(error)
util.send_message(self, "log", "error", error, error)

View File

@ -15,6 +15,7 @@
import flask import flask
import optparse, datetime import optparse, datetime
from .. import logger
class KlipperLogAnalyzer(): class KlipperLogAnalyzer():
MAXBANDWIDTH=25000. MAXBANDWIDTH=25000.
@ -81,6 +82,7 @@ class KlipperLogAnalyzer():
out.append(keyparts) out.append(keyparts)
f.close() f.close()
except IOError: except IOError:
logger.log_error(self, "Couldn't open log file: {}".format(logname))
print("Couldn't open log file") print("Couldn't open log file")
return out return out

View File

@ -0,0 +1,72 @@
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(["OctoPrintClient"], factory);
} else {
factory(global.OctoPrintClient);
}
})(this, function (OctoPrintClient) {
var OctoKlipperClient = function (base) {
this.base = base;
this.url = this.base.getBlueprintUrl("klipper");
};
OctoKlipperClient.prototype.getCfg = function (config, opts) {
return this.base.get(this.url + "config/" + config, opts);
};
OctoKlipperClient.prototype.getCfgBak = function (backup, opts) {
return this.base.get(this.url + "backup/" + backup, opts);
};
OctoKlipperClient.prototype.listCfg = function (opts) {
return this.base.get(this.url + "config/list", opts);
};
OctoKlipperClient.prototype.listCfgBak = function (opts) {
return this.base.get(this.url + "backup/list", opts);
};
OctoKlipperClient.prototype.checkCfg = function (content, opts) {
content = content || [];
var data = {
DataToCheck: content,
};
return this.base.postJson(this.url + "config/check", data, opts);
};
OctoKlipperClient.prototype.saveCfg = function (content, filename, opts) {
content = content || [];
filename = filename || [];
var data = {
DataToSave: content,
filename: filename,
};
return this.base.postJson(this.url + "config/save", data, opts);
};
OctoKlipperClient.prototype.deleteCfg = function (config, opts) {
return this.base.delete(this.url + "config/" + config, opts);
};
OctoKlipperClient.prototype.deleteBackup = function (backup, opts) {
return this.base.delete(this.url + "backup/" + backup, opts);
};
OctoKlipperClient.prototype.restoreBackup = function (backup, opts) {
return this.base.get(this.url + "backup/restore/" + backup, opts);
};
OctoKlipperClient.prototype.restoreBackupFromUpload = function (file, data) {
data = data || {};
var filename = data.filename || undefined;
return this.base.upload(this.url + "restore", file, filename, data);
};
OctoPrintClient.registerPluginComponent("klipper", OctoKlipperClient);
return OctoKlipperClient;
});

View File

@ -1,258 +1,383 @@
.plugin-klipper-sidebar { .plugin-klipper-sidebar {
padding: 1px; padding: 1px;
height: auto; height: auto;
border: 1px solid #aaa; border: 1px solid #aaa;
width: 98%; width: 98%;
text-align: center; text-align: center;
word-break: break-all; word-break: break-all;
margin: auto; margin: auto;
} }
li#navbar_plugin_klipper { li#navbar_plugin_klipper {
cursor: pointer; cursor: pointer;
max-width:360px; max-width: 360px;
max-height:80px; max-height: 80px;
word-break: break-all; word-break: break-all;
} }
.plugin-klipper-sidebar a { .plugin-klipper-sidebar a {
padding: 2px 2px; padding: 2px 2px;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
} }
.plugin-klipper-sidebar a:hover, .plugin-klipper-sidebar a:active { .plugin-klipper-sidebar a:hover,
cursor: pointer; .plugin-klipper-sidebar a:active {
} cursor: pointer;
}
.plugin-klipper-log { .plugin-klipper-log {
padding: 0px; padding: 0px;
overflow-y: scroll; overflow-y: scroll;
height: 400px; height: 400px;
border: 1px solid #eee; border: 1px solid #eee;
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
} }
.plugin-klipper-log .log-item { .plugin-klipper-log .log-item {
margin: 3px auto 0 auto; margin: 3px auto 0 auto;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 3px; border-radius: 3px;
background-color: #efefef; background-color: #efefef;
color: #333; color: #333;
} }
.plugin-klipper-log .error { .plugin-klipper-log .error {
background-color: #eebabb; background-color: #eebabb;
} }
.plugin-klipper-log .log-item .ts { .plugin-klipper-log .log-item .ts {
display: inline-block; display: inline-block;
width: 13%; width: 13%;
height: 100%; height: 100%;
vertical-align: top; vertical-align: top;
font-size: 0.8em; font-size: 0.8em;
padding: 0 0 0 5px; padding: 0 0 0 5px;
} }
.plugin-klipper-log .log-item .msg { .plugin-klipper-log .log-item .msg {
display: inline-block; display: inline-block;
width: 84%; width: 84%;
height: 100%; height: 100%;
} }
.clear-btn { .clear-btn {
margin-top: 6px; margin-top: 6px;
margin-bottom: 6px; margin-bottom: 6px;
} }
#level .controls { #level .controls {
padding: 1px; padding: 1px;
} }
ul#klipper-settings { ul#klipper-settings {
margin: 0; margin: 0;
} }
#klipper-settings a{ #klipper-settings a {
margin: 5px; margin: 5px;
} }
#tab_plugin_klipper_main .row-fluid { #tab_plugin_klipper_main .row-fluid {
display: flex; display: flex;
flex: row wrap; flex-flow: row wrap;
align-items: stretch; align-items: stretch;
} }
@media all and (max-width: 940px) { @media all and (max-width: 940px) {
#tab_plugin_klipper_main .row-fluid { #tab_plugin_klipper_main .row-fluid {
/* On small screens, we are no longer using row direction but column */ /* On small screens, we are no longer using row direction but column */
flex-direction: column; flex-direction: column;
} }
} }
#tab_plugin_klipper_main #left-side { #tab_plugin_klipper_main #left-side {
flex: 3 1; flex: 3 1;
padding-right: 10px; padding-right: 10px;
} }
#tab_plugin_klipper_main .span8 label { #tab_plugin_klipper_main .span8 label {
float: left; float: left;
} }
#tab_plugin_klipper_main #right-side { #tab_plugin_klipper_main #right-side {
flex: 1 1; flex: 1 1;
max-width: 200px; max-width: 200px;
min-width: 100px; min-width: 100px;
}
.klipper-row-fluid {
display: flex;
flex-flow: row wrap;
align-items: stretch;
}
.klipper-column-fluid {
display: flex;
flex-flow: column nowrap;
align-items: stretch;
}
.klipper-fluid-item-1 {
flex: 1 auto;
}
.klipper-fluid-item-2 {
flex: 2 auto;
}
.klipper-fluid-item-3 {
flex: 3 auto;
}
.gap {
justify-content: space-evenly;
}
@media all and (max-width: 940px) {
.klipper-row-fluid {
/* On small screens, we are no longer using row direction but column */
flex-direction: column;
}
} }
#settings_plugin_klipper { #settings_plugin_klipper {
height: 100%; height: 100%;
height: -webkit-fill-available; height: -webkit-fill-available;
}
div#klipper_backups_dialog div.modal-body textarea {
margin-bottom: 0px !important;
padding-left: 0px !important;
padding-right: 0px !important;
box-sizing: border-box;
height: -webkit-fill-available;
resize: none;
width: 100%;
} }
/* UIcustomizer fix */ /* UIcustomizer fix */
body.UICResponsiveMode #settings_dialog_content { body.UICResponsiveMode #settings_dialog_content {
height: calc(100% - 60px); height: calc(100% - 60px);
margin-right: -18px; margin-right: -18px;
margin-top: 50px; margin-top: 50px;
width: calc(100% - 15px); width: calc(100% - 15px);
} }
div#settings_plugin_klipper form { div#settings_plugin_klipper form {
margin: 0px; margin: 0px;
height: 100%; height: 100%;
} }
div#settings_plugin_klipper div.tab-content { div#settings_plugin_klipper div.tab-content {
height: calc(100% - 58px); height: calc(100% - 58px);
overflow: auto; overflow: auto;
} }
div#settings_plugin_klipper div.tab-footer { div#settings_plugin_klipper div.tab-footer {
height: 20px; height: 20px;
width: 100%; width: 100%;
top: 10px; top: 10px;
position: relative; position: relative;
border-top: 1px solid #eee; border-top: 1px solid #eee;
padding-top: 3px; padding-top: 3px;
} }
div#settings_plugin_klipper div.tab-content div#conf.tab-pane { div#settings_plugin_klipper div.tab-content div#conf.tab-pane {
height: 100%; height: 100%;
width: 100%; min-height: 200px;
width: 100%;
} }
div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group { div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group {
height: 100%; height: 100%;
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.editor-controls{ .klipper-settings-tab {
margin-bottom: 0px; height: 100%;
height: 26px; width: 100%;
} }
div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor { #settings_plugin_klipper .m-0 {
height: 95%; margin: 0;
height: calc(100% - 28px);
width: 99%;
width: calc(100% - 4px);
margin-top: 5px;
flex: 1 1;
overflow: auto;
} }
div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor div#plugin-klipper-config { #settings_plugin_klipper .scroll-y {
font-family: monospace; overflow-y: scroll;
overflow: auto;
height: 100%;
height: -webkit-fill-available;
} }
div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#conf.tab-pane.active button.btn.btn-small { #settings_plugin_klipper table {
table-layout: unset !important;
}
#settings_plugin_klipper .table-fixed thead th {
position: sticky;
top: 0;
background-color: rgba(12, 5, 5, 0.85);
color: rgb(200, 200, 200);
}
#settings_plugin_klipper .pagination-mini {
margin: 5;
}
div#klipper_editor {
height: 80%;
display: flex;
flex-flow: column;
}
@media (max-width: 979px) {
div#klipper_editor.modal {
height: unset !important;
}
}
div#klipper_editor .modal-header {
height: 6px;
}
div#klipper_editor .modal-body {
overflow: auto;
min-height: 300px;
display: flex;
flex-flow: column;
flex-grow: 1;
}
.klipper-btn-group {
display: inline-block;
}
div#klipper_editor .modal-body label.checkbox {
display: unset;
}
div#klipper_editor .modal-body input[type="text"] {
margin-bottom: 0px !important;
}
div#klipper_editor .modal-body input[type="checkbox"] {
float: unset;
margin-left: unset;
vertical-align: unset;
}
div#klipper_editor .modal-body .editor-controls {
flex: 0 auto 50px;
}
div#klipper_editor .flex-end {
align-self: flex-end;
}
div#klipper_editor div.conf-editor {
width: 99%;
width: calc(100% - 4px);
margin-top: 5px;
overflow: auto;
flex-grow : 1;
}
div#klipper_editor div.conf-editor div#plugin-klipper-config {
font-family: monospace;
overflow: auto;
}
.ace_editor {
margin: auto;
height: 200px;
width: 100%;
}
/* div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#conf.tab-pane.active button.btn.btn-small ,div#klipper_editor button.btn.btn-small{
width: 30%; width: 30%;
display: inline-block; display: inline-block;
margin: 0px 2px 2px 2px; margin: 0px 2px 2px 2px;
} } */
/*checkboxes*/ /*checkboxes*/
div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active input.inline-checkbox { div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active input.inline-checkbox,
vertical-align: -0.2em; div#klipper_editor input.inline-checkbox {
vertical-align: -0.2em;
} }
div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active label.inline { div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active label.inline,
display: inline; div#klipper_editor .inline {
display: inline;
} }
div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active div.controls input.controls-checkbox { div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active div.controls input.controls-checkbox {
margin-top: 8px; margin-top: 8px;
} }
/*macros*/ /*macros*/
div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#macros.tab-pane.active div div#item.control-group label.control-label { div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#macros.tab-pane.active div div#item.control-group label.control-label {
width: 80px; width: 80px;
} }
#macros #item.control-group { #macros #item.control-group {
margin-bottom: 2px; margin-bottom: 2px;
border: 2px solid #ccc; border: 2px solid #ccc;
border-radius: 3px; border-radius: 3px;
background-color: #eeeeee; background-color: #eeeeee;
color: #333; color: #333;
padding-bottom: 2px; padding-bottom: 2px;
padding-top: 2px; padding-top: 2px;
} }
#klipper_graph_dialog { #klipper_graph_dialog {
width: 1050px; width: 90%;
} }
#klipper_graph_dialog .full-sized-box{ #klipper_graph_dialog .full-sized-box {
width: 1000px; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
#klipper_graph_dialog form { #klipper_graph_dialog form {
margin: 0; margin: 0;
} }
#klipper_graph_dialog select { #klipper_graph_dialog select {
width: auto; width: auto;
} }
#klipper_graph_dialog .graph-footer { #klipper_graph_dialog .graph-footer {
bottom:0; bottom: 0;
} }
#klipper_graph_dialog input { #klipper_graph_dialog input {
display: inline-block; display: inline-block;
} }
#klipper_graph_dialog .status-label { #klipper_graph_dialog .status-label {
display: block; display: block;
position: absolute; position: absolute;
margin: 5px 0 0 10px; margin: 5px 0 0 10px;
} }
#klipper_graph_dialog .fill-checkbox { #klipper_graph_dialog .fill-checkbox {
display: block; display: block;
position: absolute; position: absolute;
top: 0%; top: 0%;
left: 50%; left: 50%;
} }
#klipper_graph_dialog .help-inline { #klipper_graph_dialog .help-inline {
display: block; display: block;
position: absolute; position: absolute;
top: 0px; top: 0px;
} }
#klipper_graph_canvas { #klipper_graph_canvas {
margin-top: 15px; margin-top: 15px;
} }

View File

@ -14,251 +14,236 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
$(function () { $(function () {
function KlipperViewModel(parameters) { function KlipperViewModel(parameters) {
var self = this; var self = this;
self.header = OctoPrint.getRequestHeaders({ self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json", "content-type": "application/json",
"cache-control": "no-cache" "cache-control": "no-cache",
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.settings = parameters[0];
self.loginState = parameters[1];
self.connectionState = parameters[2];
self.levelingViewModel = parameters[3];
self.paramMacroViewModel = parameters[4];
self.access = parameters[5];
self.shortStatus_navbar = ko.observable();
self.shortStatus_sidebar = 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
});
};
self.onSettingsShown = function () {
self.reloadConfig();
}
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 (!self.hasRight("MACRO")) return;
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.navbarClicked = function () {
$("#tab_plugin_klipper_main_link").find("a").click();
};
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") {
switch(data.type) {
case "PopUp":
self.showPopUp(data.subtype, data.title, data.payload);
break;
case "reload":
break;
case "console":
self.consoleMessage(data.subtype, data.payload);
break;
case "status":
if (data.payload.length > 36) {
var shortText = data.payload.substring(0, 31) + " [..]"
self.shortStatus_navbar(shortText);
} else {
self.shortStatus_navbar(data.payload);
}
self.shortStatus_sidebar(data.payload);
break;
default:
self.logMessage(data.time, data.subtype, data.payload);
self.consoleMessage(data.subtype, data.payload);
}
//if ("warningPopUp" == data.type){
// self.showPopUp(data.subtype, data.title, data.payload);
// return;
//} else if ("errorPopUp" == data.type){
// self.showPopUp(data.subtype, data.title, data.payload);
// return;
//} else if ("reload" == data.type){
// return;
//} else if ("console" == data.type) {
// self.consoleMessage(data.subtype, data.payload);
//} else if (data.type == "status") {
// self.shortStatus(data.payload);
//} else {
// self.logMessage(data.time, data.subtype, data.payload);
//}
}
};
self.logMessage = function (timestamp, type="info", message) {
if (!timestamp) {
var today = new Date();
var timestamp = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
}
self.logMessages.push({
time: timestamp,
type: type,
msg: message.replace(/\n/gi, "<br>")
});
};
self.consoleMessage = function (type, message) {
if (self.settings.settings.plugins.klipper.configuration.debug_logging() === true) {
if (type == "info"){
console.info("OctoKlipper : " + message);
} else if (type == "debug"){
console.debug("OctoKlipper : " + message);
} else {
console.error("OctoKlipper : " + message);
}
}
return
};
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 config file from Backend");
});
}
self.onClearLog = function () {
self.logMessages.removeAll();
};
self.isActive = function () {
return self.connectionState.isOperational();
};
self.hasRight = function (right_role, type) {
var arg = eval("self.access.permissions.PLUGIN_KLIPPER_" + right_role);
if (type == "Ko") {
return self.loginState.hasPermissionKo(arg);
}
return self.loginState.hasPermission(arg);
};
// OctoKlipper settings link
self.openOctoKlipperSettings = function (profile_type) {
if (!self.hasRight("CONFIG")) return;
$("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 + "']";
$(query).click();
}
};
}
OCTOPRINT_VIEWMODELS.push({
construct: KlipperViewModel,
dependencies: [
"settingsViewModel",
"loginStateViewModel",
"connectionViewModel",
"klipperLevelingViewModel",
"klipperMacroDialogViewModel",
"accessViewModel"
],
elements: [
"#tab_plugin_klipper_main",
"#sidebar_plugin_klipper",
"#navbar_plugin_klipper"
]
}); });
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.Url = OctoPrint.getBlueprintUrl("klipper");
self.settings = parameters[0];
self.loginState = parameters[1];
self.connectionState = parameters[2];
self.levelingViewModel = parameters[3];
self.paramMacroViewModel = parameters[4];
self.access = parameters[5];
self.shortStatus_navbar = ko.observable();
self.shortStatus_sidebar = 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,
icon: true
});
};
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 (!self.hasRight("MACRO")) return;
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.navbarClicked = function () {
$("#tab_plugin_klipper_main_link").find("a").click();
};
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") {
switch (data.type) {
case "PopUp":
self.showPopUp(data.subtype, data.title, data.payload);
break;
case "reload":
break;
case "console":
self.consoleMessage(data.subtype, data.payload);
break;
case "status":
if (data.payload.length > 36) {
var shortText = data.payload.substring(0, 31) + " [..]";
self.shortStatus_navbar(shortText);
} else {
self.shortStatus_navbar(data.payload);
}
self.shortStatus_sidebar(data.payload);
break;
default:
self.logMessage(data.time, data.subtype, data.payload);
self.consoleMessage(data.subtype, data.payload);
}
}
};
self.logMessage = function (timestamp, type = "info", message) {
if (!timestamp) {
var today = new Date();
var timestamp =
today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
}
self.logMessages.push({
time: timestamp,
type: type,
msg: message.replace(/\n/gi, "<br />"),
});
};
self.consoleMessage = function (type, message) {
if (
self.settings.settings.plugins.klipper.configuration.debug_logging() === true
) {
if (type == "info") {
console.info("OctoKlipper : " + message);
} else if (type == "debug") {
console.debug("OctoKlipper : " + message);
} else {
console.error("OctoKlipper : " + message);
}
}
return;
};
self.onClearLog = function () {
self.logMessages.removeAll();
};
self.isActive = function () {
return self.connectionState.isOperational();
};
self.hasRight = function (right_role) {
//if (self.loginState.isAdmin) return true;
if (right_role == "CONFIG") {
return self.loginState.hasPermission(
self.access.permissions.PLUGIN_KLIPPER_CONFIG
);
} else if (right_role == "MACRO") {
return self.loginState.hasPermission(
self.access.permissions.PLUGIN_KLIPPER_MACRO
);
}
};
self.hasRightKo = function (right_role) {
//if (self.loginState.isAdmin) return true;
if (right_role == "CONFIG") {
return self.loginState.hasPermissionKo(
self.access.permissions.PLUGIN_KLIPPER_CONFIG
);
} else if (right_role == "MACRO") {
return self.loginState.hasPermissionKo(
self.access.permissions.PLUGIN_KLIPPER_MACRO
);
}
};
// OctoKlipper settings link
self.openOctoKlipperSettings = function (profile_type) {
self.consoleMessage("debug", ": openOctoKlipperSettings :");
if (!self.hasRight("CONFIG")) return;
self.consoleMessage("debug", ": openOctoKlipperSettings : Access okay");
$("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 + "']";
$(query).click();
}
};
}
OCTOPRINT_VIEWMODELS.push({
construct: KlipperViewModel,
dependencies: [
"settingsViewModel",
"loginStateViewModel",
"connectionViewModel",
"klipperLevelingViewModel",
"klipperMacroDialogViewModel",
"accessViewModel",
],
elements: [
"#tab_plugin_klipper_main",
"#sidebar_plugin_klipper",
"#navbar_plugin_klipper",
],
});
}); });

View File

@ -0,0 +1,297 @@
// <Octoprint Klipper Plugin>
// This program is free software: you can redistribute it and/or modify
// 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 <https://www.gnu.org/licenses/>.
$(function () {
function KlipperBackupViewModel(parameters) {
var self = this;
self.loginState = parameters[0];
self.klipperViewModel = parameters[1];
self.access = parameters[2];
self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json",
"cache-control": "no-cache",
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.Url = OctoPrint.getBlueprintUrl("klipper");
self.markedForFileRestore = ko.observableArray([]);
self.CfgContent = ko.observable();
//uploads
self.maxUploadSize = ko.observable(0);
self.backupUploadData = undefined;
self.backupUploadName = ko.observable();
self.isAboveUploadSize = function (data) {
return data.size > self.maxUploadSize();
};
self.onStartupComplete = function () {
if (self.loginState.loggedIn()) {
self.listBakFiles();
}
};
// initialize list helper
self.backups = new ItemListHelper(
"klipperBakFiles",
{
name: function (a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
},
date: function (a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
size: function (a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
},
},
{},
"name",
[],
[],
5
);
self.listBakFiles = function () {
self.klipperViewModel.consoleMessage("debug", "listBakFiles:");
OctoPrint.plugins.klipper.listCfgBak()
.done(function (response) {
self.klipperViewModel.consoleMessage("debug", "listBakFilesdone: " + response);
self.backups.updateItems(response.files);
self.backups.resetPage();
});
};
self.showCfg = function (backup) {
if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return;
OctoPrint.plugins.klipper.getCfgBak(backup).done(function (response) {
$('#klipper_backups_dialog textarea').attr('rows', response.response.config.split(/\r\n|\r|\n/).length);
self.CfgContent(response.response.config);
});
};
self.removeCfg = function (backup) {
if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return;
var perform = function () {
OctoPrint.plugins.klipper
.deleteBackup(backup)
.done(function () {
self.listBakFiles();
})
.fail(function (response) {
var html = "<p>" + _.sprintf(gettext("Failed to remove config %(name)s.</p><p>Please consult octoprint.log for details.</p>"), { name: _.escape(backup) });
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + _.escape(response.responseText) + "</pre>");
new PNotify({
title: gettext("Could not remove config"),
text: html,
type: "error",
hide: false,
});
});
};
showConfirmationDialog(
_.sprintf(gettext('You are about to delete backuped config file "%(name)s".'), {
name: _.escape(backup),
}),
perform
);
};
self.restoreBak = function (backup) {
if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return;
var restore = function () {
OctoPrint.plugins.klipper.restoreBackup(backup).done(function (response) {
self.klipperViewModel.consoleMessage("debug", "restoreCfg: " + response.text);
});
};
var html = "<p>" + gettext("This will overwrite any file with the same name on the configpath.") + "</p>" + "<p>" + backup + "</p>";
showConfirmationDialog({
title: gettext("Are you sure you want to restore now?"),
html: html,
proceed: gettext("Proceed"),
onproceed: restore(),
});
};
self.markFilesOnPage = function () {
self.markedForFileRestore(_.uniq(self.markedForFileRestore().concat(_.map(self.backups.paginatedItems(), "file"))));
};
self.markAllFiles = function () {
self.markedForFileRestore(_.map(self.backups.allItems, "file"));
};
self.clearMarkedFiles = function () {
self.markedForFileRestore.removeAll();
};
self.restoreMarkedFiles = function () {
var perform = function () {
self._bulkRestore(self.markedForFileRestore()).done(function () {
self.markedForFileRestore.removeAll();
});
};
showConfirmationDialog(
_.sprintf(gettext("You are about to restore %(count)d backuped config files."), {
count: self.markedForFileRestore().length,
}),
perform
);
};
self.removeMarkedFiles = function () {
var perform = function () {
self._bulkRemove(self.markedForFileRestore()).done(function () {
self.markedForFileRestore.removeAll();
});
};
showConfirmationDialog(
_.sprintf(gettext("You are about to delete %(count)d backuped config files."), {
count: self.markedForFileRestore().length,
}),
perform
);
};
self._bulkRestore = function (files) {
var title, message, handler;
title = gettext("Restoring klipper config files");
self.klipperViewModel.consoleMessage("debug", title);
message = _.sprintf(gettext("Restoring %(count)d backuped config files..."), {
count: files.length,
});
handler = function (filename) {
return OctoPrint.plugins.klipper
.restoreBackup(filename)
.done(function (response) {
deferred.notify(
_.sprintf(gettext("Restored %(filename)s..."), {
filename: _.escape(filename),
}),
true
);
self.klipperViewModel.consoleMessage("debug", "restoreCfg: " + response.text);
self.markedForFileRestore.remove(function (item) {
return item.name == filename;
});
})
.fail(function () {
deferred.notify(_.sprintf(gettext("Restoring of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false);
});
};
var deferred = $.Deferred();
var promise = deferred.promise();
var options = {
title: title,
message: message,
max: files.length,
output: true,
};
showProgressModal(options, promise);
var requests = [];
_.each(files, function (filename) {
var request = handler(filename);
requests.push(request);
});
$.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () {
deferred.resolve();
});
return promise;
};
self._bulkRemove = function (files) {
var title, message, handler;
title = gettext("Deleting backup files");
message = _.sprintf(gettext("Deleting %(count)d backup files..."), {
count: files.length,
});
handler = function (filename) {
return OctoPrint.plugins.klipper
.deleteBackup(filename)
.done(function () {
deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), { filename: _.escape(filename) }), true);
self.markedForFileRestore.remove(function (item) {
return item.name == filename;
});
})
.fail(function () {
deferred.notify(_.sprintf(gettext("Deleting of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false);
});
};
var deferred = $.Deferred();
var promise = deferred.promise();
var options = {
title: title,
message: message,
max: files.length,
output: true,
};
showProgressModal(options, promise);
var requests = [];
_.each(files, function (filename) {
var request = handler(filename);
requests.push(request);
});
$.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () {
deferred.resolve();
self.listBakFiles();
});
return promise;
};
}
OCTOPRINT_VIEWMODELS.push({
construct: KlipperBackupViewModel,
dependencies: ["loginStateViewModel", "klipperViewModel", "accessViewModel"],
elements: ["#klipper_backups_dialog"],
});
});

View File

@ -0,0 +1,234 @@
// <Octoprint Klipper Plugin>
// This program is free software: you can redistribute it and/or modify
// 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 <https://www.gnu.org/licenses/>.
$(function () {
function KlipperEditorViewModel(parameters) {
var self = this;
var obKlipperConfig = null;
var editor = null;
self.settings = parameters[0];
self.klipperViewModel = parameters[1];
self.CfgFilename = ko.observable("");
self.CfgContent = ko.observable("");
self.config = []
self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json",
"cache-control": "no-cache",
});
self.process = function (config) {
self.config = config;
self.CfgFilename(config.file);
self.CfgContent(config.content);
if (editor) {
editor.session.setValue(config.content);
editor.clearSelection();
}
setInterval(function () {
if (editor) {
var modalbodyheight = $('#klipper_editor').height();
//$('#conf_editor').height( modalbodyheight - 135 );
editor.resize();
};
}, 500);
};
self.checkSyntax = function () {
if (editor.session) {
self.klipperViewModel.consoleMessage("debug", "checkSyntax:");
OctoPrint.plugins.klipper.checkCfg(editor.session.getValue())
.done(function (response) {
var msg = ""
if (response.is_syntax_ok == true) {
msg = gettext('Syntax OK')
} else {
msg = gettext('Syntax NOK')
}
showMessageDialog(
msg,
{
title: gettext("SyntaxCheck")
}
)
});
};
};
self.saveCfg = function () {
if (editor.session) {
self.klipperViewModel.consoleMessage("debug", "Save:");
OctoPrint.plugins.klipper.saveCfg(editor.session.getValue(), self.CfgFilename())
.done(function (response) {
var msg = ""
if (response.saved === true) {
msg = gettext('File saved.')
} else {
msg = gettext('File not saved.')
}
showMessageDialog(
msg,
{
title: gettext("Save File")
}
)
});
}
};
self.minusFontsize = function () {
self.settings.settings.plugins.klipper.configuration.fontsize(
self.settings.settings.plugins.klipper.configuration.fontsize() - 1
);
if (self.settings.settings.plugins.klipper.configuration.fontsize() < 9) {
self.settings.settings.plugins.klipper.configuration.fontsize(9);
}
if (editor) {
editor.setFontSize(
self.settings.settings.plugins.klipper.configuration.fontsize()
);
editor.resize();
}
};
self.plusFontsize = function () {
self.settings.settings.plugins.klipper.configuration.fontsize(
self.settings.settings.plugins.klipper.configuration.fontsize() + 1
);
if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) {
self.settings.settings.plugins.klipper.configuration.fontsize(20);
}
if (editor) {
editor.setFontSize(
self.settings.settings.plugins.klipper.configuration.fontsize()
);
editor.resize();
}
};
self.loadLastSession = function () {
if (self.settings.settings.plugins.klipper.configuration.temp_config() != "") {
self.klipperViewModel.consoleMessage(
"info",
"lastSession:" +
self.settings.settings.plugins.klipper.configuration.temp_config()
);
if (editor.session) {
editor.session.setValue(
self.settings.settings.plugins.klipper.configuration.temp_config()
);
editor.clearSelection();
}
}
};
self.reloadFromFile = function () {
OctoPrint.plugins.klipper.getCfg(self.CfgFilename())
.done(function (response) {
self.klipperViewModel.consoleMessage("debug", "reloadFromFile: " + response);
if (response.response.text != "") {
var msg = response.response.text
showMessageDialog(
msg,
{
title: gettext("Reload File")
}
)
} else {
if (editor) {
editor.session.setValue(response.response.config);
editor.clearSelection();
}
}
})
.fail(function (response) {
var msg = response
showMessageDialog(
msg,
{
title: gettext("Reload File")
}
)
});
};
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.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize());
editor.resize();
editor.clearSelection();
}
return obKlipperConfig;
};
self.onStartup = function () {
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.clearSelection();
editor.setOptions({
hScrollBarAlwaysVisible: false,
vScrollBarAlwaysVisible: false,
autoScrollEditorIntoView: true,
showPrintMargin: false,
maxLines: "Infinity",
minLines: 100
//maxLines: "Infinity"
});
editor.session.on('change', function (delta) {
if (obKlipperConfig) {
obKlipperConfig.silentUpdate(editor.getValue());
editor.resize();
}
});
};
}
OCTOPRINT_VIEWMODELS.push({
construct: KlipperEditorViewModel,
dependencies: ["settingsViewModel", "klipperViewModel"],
elements: ["#klipper_editor"],
});
});

View File

@ -13,243 +13,293 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
$(function() { $(function () {
$('#klipper-settings a:first').tab('show'); $("#klipper-settings a:first").tab("show");
function KlipperSettingsViewModel(parameters) { function KlipperSettingsViewModel(parameters) {
var self = this; var self = this;
var obKlipperConfig = null;
var editor = null;
self.settings = parameters[0]; self.settings = parameters[0];
self.klipperViewModel = parameters[1]; self.klipperViewModel = parameters[1];
self.klipperEditorViewModel = parameters[2];
self.klipperBackupViewModel = parameters[3];
self.access = parameters[4];
self.header = OctoPrint.getRequestHeaders({ self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json", "content-type": "application/json",
"cache-control": "no-cache" "cache-control": "no-cache",
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.onSettingsBeforeSave = function () {
if (editor.session) {
self.klipperViewModel.consoleMessage("debug", "onSettingsBeforeSave:")
var settings = {
"crossDomain": true,
"url": self.apiUrl,
"method": "POST",
"headers": self.header,
"processData": false,
"dataType": "json",
"data": JSON.stringify({command: "checkConfig",
config: editor.session.getValue()})
}
$.ajax(settings).done(function (response) {
});
}
}
self.addMacro = function() {
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.moveMacroUp = function(macro) {
self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro)
}
self.moveMacroDown = function(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.removeProbePoint = function(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.moveProbePointDown = function(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]);
}
}
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]);
}
}
self.minusFontsize = function () {
self.settings.settings.plugins.klipper.configuration.fontsize(self.settings.settings.plugins.klipper.configuration.fontsize() - 1);
if (self.settings.settings.plugins.klipper.configuration.fontsize() < 9) {
self.settings.settings.plugins.klipper.configuration.fontsize(9);
}
if (editor) {
editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize());
editor.resize();
}
}
self.plusFontsize = function () {
self.settings.settings.plugins.klipper.configuration.fontsize(self.settings.settings.plugins.klipper.configuration.fontsize() + 1);
if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) {
self.settings.settings.plugins.klipper.configuration.fontsize(20);
}
if (editor) {
editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize());
editor.resize();
}
}
self.loadLastSession = function () {
if (self.settings.settings.plugins.klipper.configuration.old_config() != "") {
self.klipperViewModel.consoleMessage("info","lastSession:" + self.settings.settings.plugins.klipper.configuration.old_config())
if (editor.session) {
editor.session.setValue(self.settings.settings.plugins.klipper.configuration.old_config());
editor.clearSelection();
}
}
}
self.reloadFromFile = function () {
if (editor.session) {
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) {
if (editor.session) {
editor.session.setValue(response["data"]);
editor.clearSelection();
}
});
}
}
self.loadCfgBackup = function () {
if (editor.session) {
var settings = {
"crossDomain": true,
"url": self.apiUrl,
"method": "POST",
"headers": self.header,
"processData": false,
"dataType": "json",
"data": JSON.stringify({command: "reloadCfgBackup"})
}
$.ajax(settings).done(function (response) {
if (editor.session) {
editor.session.setValue(response["data"]);
editor.clearSelection();
}
});
}
}
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.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize());
editor.resize();
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({
hScrollBarAlwaysVisible: false,
vScrollBarAlwaysVisible: false,
autoScrollEditorIntoView: true,
showPrintMargin: false,
//maxLines: "Infinity"
})
editor.session.on('change', function(delta) {
if (obKlipperConfig) {
obKlipperConfig.silentUpdate(editor.getValue());
editor.resize();
}
});
// 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
}
}
}
}
OCTOPRINT_VIEWMODELS.push({
construct: KlipperSettingsViewModel,
dependencies: [
"settingsViewModel",
"klipperViewModel"
],
elements: ["#settings_plugin_klipper"]
}); });
self.markedForFileRemove = ko.observableArray([]);
// initialize list helper
self.configs = new ItemListHelper(
"klipperCfgFiles",
{
name: function (a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
},
date: function (a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
size: function (a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
},
},
{},
"name",
[],
[],
15
);
self.onStartupComplete = function () {
self.listCfgFiles();
};
self.listCfgFiles = function () {
self.klipperViewModel.consoleMessage("debug", "listCfgFiles:");
OctoPrint.plugins.klipper.listCfg().done(function (response) {
self.klipperViewModel.consoleMessage("debug", "listCfgFiles: " + response);
self.configs.updateItems(response.files);
self.configs.resetPage();
});
};
self.removeCfg = function (config) {
if (!self.klipperViewModel.hasRight("CONFIG")) return;
var perform = function () {
OctoPrint.plugins.klipper
.deleteCfg(config)
.done(function () {
self.listCfgFiles();
var html = "<p>" + _.sprintf(gettext("All fine</p>"), { name: _.escape(config) });
new PNotify({
title: gettext("All is fine"),
text: html,
type: "error",
hide: false,
});
})
.fail(function (response) {
var html = "<p>" + _.sprintf(gettext("Failed to remove config %(name)s.</p><p>Please consult octoprint.log for details.</p>"), { name: _.escape(config) });
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + _.escape(response.responseText) + "</pre>");
new PNotify({
title: gettext("Could not remove config"),
text: html,
type: "error",
hide: false,
});
});
};
showConfirmationDialog(
_.sprintf(gettext('You are about to delete config file "%(name)s".'), {
name: _.escape(config),
}),
perform
);
};
self.markFilesOnPage = function () {
self.markedForFileRemove(_.uniq(self.markedForFileRemove().concat(_.map(self.configs.paginatedItems(), "file"))));
};
self.markAllFiles = function () {
self.markedForFileRemove(_.map(self.configs.allItems, "file"));
};
self.clearMarkedFiles = function () {
self.markedForFileRemove.removeAll();
};
self.removeMarkedFiles = function () {
var perform = function () {
self._bulkRemove(self.markedForFileRemove()).done(function () {
self.markedForFileRemove.removeAll();
});
};
showConfirmationDialog(
_.sprintf(gettext("You are about to delete %(count)d config files."), {
count: self.markedForFileRemove().length,
}),
perform
);
};
self._bulkRemove = function (files) {
var title, message, handler;
title = gettext("Deleting config files");
message = _.sprintf(gettext("Deleting %(count)d config files..."), {
count: files.length,
});
handler = function (filename) {
return OctoPrint.plugins.klipper
.deleteCfg(filename)
.done(function () {
deferred.notify(
_.sprintf(gettext("Deleted %(filename)s..."), {
filename: _.escape(filename),
}),
true
);
self.markedForFileRemove.remove(function (item) {
return item.name == filename;
});
})
.fail(function () {
deferred.notify(_.sprintf(gettext("Deleting of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false);
});
};
var deferred = $.Deferred();
var promise = deferred.promise();
var options = {
title: title,
message: message,
max: files.length,
output: true,
};
showProgressModal(options, promise);
var requests = [];
_.each(files, function (filename) {
var request = handler(filename);
requests.push(request);
});
$.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () {
deferred.resolve();
self.listCfgFiles();
});
return promise;
};
self.showBackupsDialog = function () {
self.klipperViewModel.consoleMessage("debug", "showBackupsDialog:");
self.klipperBackupViewModel.listBakFiles();
var dialog = $("#klipper_backups_dialog");
dialog.modal({
show: "true",
minHeight: "600px",
});
};
self.newFile = function () {
if (!self.klipperViewModel.hasRight("CONFIG")) return;
var config = {
content: "",
file: "Change Filename",
};
self.klipperEditorViewModel.process(config);
var editorDialog = $("#klipper_editor");
editorDialog.modal({
show: "true",
backdrop: "static",
});
};
self.showEditUserDialog = function (file) {
if (!self.klipperViewModel.hasRight("CONFIG")) return;
OctoPrint.plugins.klipper.getCfg(file).done(function (response) {
var config = {
content: response.response.config,
file: file,
};
self.klipperEditorViewModel.process(config);
var editorDialog = $("#klipper_editor");
editorDialog.modal({
show: "true",
backdrop: "static",
});
});
};
self.addMacro = function () {
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.moveMacroUp = function (macro) {
self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro);
};
self.moveMacroDown = function (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.removeProbePoint = function (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.moveProbePointDown = function (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]);
}
};
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]);
}
};
self.onDataUpdaterPluginMessage = function (plugin, data) {
if (plugin == "klipper" && data.type == "reload" && data.subtype == "configlist") {
self.klipperViewModel.consoleMessage("debug", "onDataUpdaterPluginMessage klipper reload configlist");
self.listCfgFiles();
}
};
}
OCTOPRINT_VIEWMODELS.push({
construct: KlipperSettingsViewModel,
dependencies: ["settingsViewModel", "klipperViewModel", "klipperEditorViewModel", "klipperBackupViewModel", "accessViewModel"],
elements: ["#settings_plugin_klipper"],
});
}); });

View File

@ -0,0 +1,92 @@
<div id="klipper_backups_dialog" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_backups_dialog_label"
aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_dialog_label">{{ _('Backups') }}</h3>
</div>
<div class="modal-body klipper-column-fluid">
<div class="klipper-row-fluid">
<div class="klipper-fluid-item-2" data-bind="visible: $root.klipperViewModel.hasRightKo('CONFIG')">
<button class="btn btn-small" data-bind="click: listBakFiles" title="{{ _('Refresh file list') }}"><i class="icon-refresh"></i>
Refresh</button>
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="far fa-square"></i> <span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0)" data-bind="click: markFilesOnPage">{{ _('Select all on this page') }}</a>
</li>
<li><a href="javascript:void(0)" data-bind="click: markAllFiles">{{ _('Select all') }}</a></li>
<li class="divider"></li>
<li><a href="javascript:void(0)" data-bind="click: clearMarkedFiles">{{ _('Clear selection') }}</a></li>
</ul>
</div>
<button class="btn btn-small"
data-bind="click: restoreMarkedFiles, enable: markedForFileRestore().length > 0">{{ _('Restore selected') }}</button>
<button class="btn btn-small" data-bind="click: removeMarkedFiles, enable: markedForFileRestore().length > 0">{{
_('Delete selected') }}</button>
</div>
<div class="pull-right">
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" data-bind="click: function() { backups.changeSorting('name'); }"><i class="fas fa-check"
data-bind="style: {visibility: backups.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }}
({{ _('ascending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { backups.changeSorting('date'); }"><i class="fas fa-check"
data-bind="style: {visibility: backups.currentSorting() == 'date' ? 'visible' : 'hidden'}"></i> {{ _('Sort by date') }}
({{ _('descending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { backups.changeSorting('size'); }"><i class="fas fa-check"
data-bind="style: {visibility: backups.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }}
({{ _('descending') }})</a></li>
</ul>
</div>
</div>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="klipper_bak_files">
<thead>
<tr>
<th class="klipper_baks_checkbox"></th>
<th class="klipper_baks_name">{{ _('Name') }}</th>
<th class="klipper_baks_size">{{ _('Size') }}</th>
<th class="klipper_baks_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: backups.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="klipper_baks_checkbox"><input type="checkbox"
data-bind="value: file, checked: $root.markedForFileRestore, invisible: !$root.klipperViewModel.hasRightKo('CONFIG')" />
</td>
<td class="klipper_baks_name" data-bind="text: name"></td>
<td class="klipper_baks_size" data-bind="text: size"></td>
<td class="klipper_baks_action">
<a href="javascript:void(0)" class="far fa-trash-alt" title="{{ _('Delete') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.removeCfg($data.name); }"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-download" title="{{ _('Restore') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.restoreBak($data.name); }"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-camera" title="{{ _('Preview') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.showCfg($data.name); }"></a>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: backups.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: backups.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: backups.pages">
<li data-bind="css: { active: $data.number === $root.backups.currentPage(), disabled: $data.number === -1 }"><a href="javascript:void(0)"
data-bind="text: $data.text, click: function() { $root.backups.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: backups.currentPage() === backups.lastPage()}"><a href="javascript:void(0)"
data-bind="click: backups.nextPage">»</a></li>
</ul>
</div>
<div class="klipper-fluid-item-3">
<textarea readonly data-bind="value: CfgContent" id="klipper_bak_text"></textarea>
</div>
</div>
<div class="modal-footer">
</div>
</div>

View File

@ -0,0 +1,45 @@
<div id="klipper_editor" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_editor_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_dialog_label">{{ _('Editor') }}</h3>
</div>
<div class="modal-body">
<script src="plugin/klipper/static/js/lib/ace/ace.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/theme-monokai.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/mode-klipper_config.js" type="text/javascript"></script>
<div class="editor-controls">
<span class="control-label">Filename:</span>
<input type="text" data-bind="value: CfgFilename">
<div class="klipper-btn-group">
<button class="btn btn-small" data-bind="click: saveCfg" title="{{ _('Save Config') }}">
<i class="fas fa-save"></i> {{ _('Save Config') }}
</button>
<button class="btn btn-small" data-bind="click: loadLastSession" title="{{ _('Reload last version') }}">
<i class="fas fa-redo"></i> {{ _('Reload last version') }}
</button>
<button class="btn btn-small" data-bind="click: reloadFromFile" title="{{ _('Reload from file') }}">
<i class="fas fa-upload"></i> {{ _('Reload from file') }}
</button>
<button class="btn btn-small" data-bind="click: checkSyntax" title="{{ _('Check Syntax') }}">
<i class="fas fa-spell-check"></i> {{ _('Check Syntax') }}
</button>
</div>
<span class="pull-right">
<label class="checkbox">
<input class="inline-checkbox" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.parse_check">
{{ _('Check parsing on save') }}
</label>
<a href='#' style="text-decoration: none;" data-bind="click: minusFontsize" title="{{ _('Decrease Fontsize') }}">
<i class="fas fa-search-minus"></i>
</a>
<a href='#' style="text-decoration: none;" data-bind="click: plusFontsize" title="{{ _('Increase Fontsize') }}">
<i class="fas fa-search-plus"></i>
</a>
</span>
</div>
<div class="conf-editor" id="conf_editor">
<input id="hdnLoadKlipperConfig" type="hidden" data-bind="value: configBound(CfgContent)">
<div id="plugin-klipper-config"></div>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div id="klipper_graph_dialog" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_graph_dialog_label" aria-hidden="true"> <div id="klipper_graph_dialog" class="modal hide fade large" tabindex="-1" role="dialog" aria-labelledby="klipper_graph_dialog_label" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Performance Graph') }}</h3> <h3 id="klipper_dialog_label">{{ _('Performance Graph') }}</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="full-sized-box"> <div class="full-sized-box">

View File

@ -1,7 +1,7 @@
<div id="klipper_offset_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_offset_dialog_label" aria-hidden="true"> <div id="klipper_offset_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_offset_dialog_label" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Coordinate Offset') }}</h3> <h3 id="klipper_dialog_label">{{ _('Coordinate Offset') }}</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="row-fluid" style="margin-bottom: 15px"> <div class="row-fluid" style="margin-bottom: 15px">

View File

@ -1,7 +1,7 @@
<div id="klipper_macro_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_macro_dialog_label" aria-hidden="true"> <div id="klipper_macro_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_macro_dialog_label" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Run - ') }}<span data-bind="text: macroName"></span></h3> <h3 id="klipper_dialog_label">{{ _('Run - ') }}<span data-bind="text: macroName"></span></h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="control-group" data-bind="foreach: parameters"> <div class="control-group" data-bind="foreach: parameters">

View File

@ -1,7 +1,7 @@
<div id="klipper_pid_tuning_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_pid_tuning_dialog_label" aria-hidden="true"> <div id="klipper_pid_tuning_dialog" class="modal hide fade small" tabindex="-1" role="dialog" aria-labelledby="klipper_pid_tuning_dialog_label" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="klipper_pid_tuning_dialog_label">{{ _('PID Tuning') }}</h3> <h3 id="klipper_dialog_label">{{ _('PID Tuning') }}</h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="control-group"> <div class="control-group">

View File

@ -1,236 +1,317 @@
<form class="form-horizontal"> <form class="form-horizontal">
<ul class="nav nav-pills" id="klipper-settings"> <ul class="nav nav-pills" id="klipper-settings">
<li><a href="#basic" data-toggle="tab" data-profile-type="klipper-basic">{{ _('Basic') }}</a></li> <li><a href="#basic" data-toggle="tab" data-profile-type="klipper-basic">{{ _('Basic') }}</a></li>
<li><a href="#macros" data-toggle="tab" data-profile-type="klipper-macros">{{ _('Macros') }}</a></li> <li><a href="#macros" data-toggle="tab" data-profile-type="klipper-macros">{{ _('Macros') }}</a></li>
<li><a href="#level" data-toggle="tab" data-profile-type="klipper-bed">{{ _('Bed Leveling') }}</a></li> <li><a href="#level" data-toggle="tab" data-profile-type="klipper-bed">{{ _('Bed Leveling') }}</a></li>
<li><a href="#conf" data-toggle="tab" data-profile-type="klipper-config">{{ _('Klipper Configuration') }}</a></li> <li><a href="#conf" data-toggle="tab" data-profile-type="klipper-config">{{ _('Klipper Configuration') }}</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<!-- Basics --> <!-- Basics -->
<div class="tab-pane active" id="basic"> <div class="tab-pane active" id="basic">
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Serial Port') }}</label> <label class="control-label">{{ _('Serial Port') }}</label>
<div class="controls"> <div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.connection.port"> <input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.connection.port" />
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Replace Connection Panel') }}</label> <label class="control-label">{{ _('Replace Connection Panel') }}</label>
<div class="controls"> <div class="controls">
<input class="controls-checkbox" title="{{ _('Replace Connection Panel') }}" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.connection.replace_connection_panel"> <input class="controls-checkbox" title="{{ _('Replace Connection Panel') }}" type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.connection.replace_connection_panel">
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Show Short Messages') }}</label> <label class="control-label">{{ _('Show Short Messages') }}</label>
<div class="controls"> <div class="controls">
<label class="checkbox" title="{{ _('on NavBar') }}"><input type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_navbar"> {{ _('on NavBar') }}</label> <label class="checkbox" title="{{ _('on NavBar') }}"><input type="checkbox"
<label class="checkbox" title="{{ _('on SideBar') }}"><input type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_sidebar"> {{ _('on SideBar') }}</label> data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_navbar" /> {{ _('on NavBar') }}</label>
<label class="checkbox" title="{{ _('on SideBar') }}"><input type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.configuration.shortStatus_sidebar" /> {{ _('on SideBar') }}</label>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Enable debug logging') }}</label> <label class="control-label">{{ _('Enable debug logging') }}</label>
<div class="controls"> <div class="controls">
<input class="controls-checkbox" title="{{ _('Enable debug logging') }}" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.debug_logging"> <input class="controls-checkbox" title="{{ _('Enable debug logging') }}" type="checkbox"
data-bind="checked: settings.settings.plugins.klipper.configuration.debug_logging" />
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Klipper Config File') }}</label> <label class="control-label">{{ _('Klipper Config File') }}</label>
<div class="controls"> <div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.configpath"> <input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.configpath" />
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Klipper Log File') }}</label> <label class="control-label">{{ _('Klipper Log File') }}</label>
<div class="controls"> <div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.logpath"> <input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.logpath" />
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Configuration Reload Command') }}</label> <label class="control-label">{{ _('Configuration Reload Command') }}</label>
<div class="controls"> <div class="controls">
<select data-bind="value: settings.settings.plugins.klipper.configuration.reload_command"> <select data-bind="value: settings.settings.plugins.klipper.configuration.reload_command">
<option value="RESTART">RESTART</option> <option value="RESTART">RESTART</option>
<option value="FIRMWARE_RESTART">FIRMWARE_RESTART</option> <option value="FIRMWARE_RESTART">FIRMWARE_RESTART</option>
<option value="manually">Manually</option> <option value="manually">Manually</option>
</select> </select>
<span class="help-block"> <span class="help-block">
{{ _('The command that is executed when the Klipper configuration changed and needs to be reloaded.<br> {{ _('The command that is executed when the Klipper configuration changed and needs to be reloaded.<br />
Set this to "Manually" if you don\'t want to immediately restart klipper.') }} Set this to "Manually" if you don\'t want to immediately restart klipper.') }}
</span> </span>
</div> </div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Config Backup') }}</label>
<div class="controls">
<button class="btn btn-small" data-bind='click: showBackupsDialog' title="{{ _('Show Backups') }}">
<i class="fas fa-upload"></i> {{ _('Show Backups') }}
</button>
</div>
</div>
</div> </div>
</div> <!-- Macros -->
<!-- Macros --> <div class="tab-pane" id="macros">
<div class="tab-pane" id="macros"> <div class="control-group" style="margin-bottom: 0px;">
<div class="control-group" style="margin-bottom: 0px;"> <div class="controls" style="margin-left: 82px;">
<div class="controls" style="margin-left: 82px;">
<div class="row-fluid"> <div class="row-fluid">
<div class="span8" style="text-align: right"><small>{{ _('Add macro button to:') }}</small></div> <div class="span8" style="text-align: right"><small>{{ _('Add macro button to:') }}</small></div>
<div class="span1" style="margin: auto;text-align: center"><small>{{ _('Klipper Tab') }}</small></div> <div class="span1" style="margin: auto;text-align: center"><small>{{ _('Klipper Tab') }}</small></div>
<div class="span2" style="margin: auto;"><small>{{ _('Sidebar') }}</small></div> <div class="span2" style="margin: auto;"><small>{{ _('Sidebar') }}</small></div>
</div> </div>
</div>
</div>
<div data-bind="foreach: settings.settings.plugins.klipper.macros">
<div class="control-group" id="item">
<label class="control-label">{{ _('Name') }}</label>
<div class="controls" style="margin-left: 82px;">
<div class="row-fluid">
<div class="span8">
<input type="text" class="input-block-level" data-bind="value: name"/>
</div>
<div class="span1" style="margin: auto; text-align: center;">
<input title="{{ _('Klipper Tab') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: tab"/>
</div>
<div class="span1" style="margin: auto; text-align: center;">
<input title="{{ _('Sidebar') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: sidebar"/>
</div>
<div class="span2" style="margin: auto; text-align: center;">
<a href='#' style="vertical-align: bottom;" data-bind='click: $parent.moveMacroUp' class="fa fa-chevron-up"></a>
<a href='#' style="vertical-align: bottom;" data-bind='click: $parent.moveMacroDown' class="fa fa-chevron-down"></a>
<a href='#' style="vertical-align: bottom;" data-bind='click: $parent.removeMacro' class="fa fa-trash-o"></a>
</div>
</div>
</div>
<label class="control-label">{{ _('Command') }}</label>
<div class="controls" style="margin-left: 82px;">
<div class="row-fluid">
<div class="span12" style="margin-top:2px;">
<textarea rows="2" class="block" data-bind="value: macro">
</textarea>
</div>
</div>
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<a href='#' data-bind='click: addMacro' title="{{ _('Add Macro') }}" class="fa fa-plus-circle"></a> {{ _('Add Macro') }}
</div>
</div>
<div class="control-group">
<span class="help-block">
{{ _('To show a dialog that asks for parameters you can write your macro like in the following example:') }}<br>
</span>
</div>
<div class="control-group">
<pre>
PID_CALIBRATE
HEATER={label:Heater, default:extruder, options:extruder|extruder1}
TARGET={label:Target Temperature, unit:°C, default:190}
WRITE_FILE={label:Write to File, default:0, options:0|1}
</pre>
</div>
</div>
<!-- Leveling -->
<div class="tab-pane" id="level">
<div class="control-group">
<span class="help-block">
{{ _('This feature assists in manually leveling you print bed by moving the head to the defined points in sequence.<br>
If you use a piece of paper for leveling, set "Probe Height" to the paper thickness eg. "0.1".') }}
</span>
</div>
<div class="control-group">
<label class="control-label">{{ _('Probe Height') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.height">
<span class="add-on">mm</span>
</div>
<span class="help-inline">{{ _('Z-height to probe at') }}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Probe Lift') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.lift">
<span class="add-on">mm</span>
</div>
<span class="help-inline">{{ _('Lift Head by this amount before moving.') }}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Probe Feedrate Z') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_z">
<span class="add-on">mm/min</span>
</div> </div>
</div> </div>
</div> <div data-bind="foreach: settings.settings.plugins.klipper.macros">
<div class="control-group"> <div class="control-group" id="item">
<label class="control-label">{{ _('Feedrate X/Y') }}</label> <label class="control-label">{{ _('Name') }}</label>
<div class="controls"> <div class="controls" style="margin-left: 82px;">
<div class="input-append"> <div class="row-fluid">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_xy"> <div class="span8">
<span class="add-on">mm/min</span> <input type="text" class="input-block-level" data-bind="value: name" />
</div> </div>
</div> <div class="span1" style="margin: auto; text-align: center;">
</div> <input title="{{ _('Klipper Tab') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: tab" />
<div class="control-group"> </div>
<h5>{{ _('Probe Points') }}</h5> <div class="span1" style="margin: auto; text-align: center;">
<div class="controls"> <input title="{{ _('Sidebar') }}" style="margin: auto;" type="checkbox" class="input-block-level" data-bind="checked: sidebar" />
<div class="row-fluid"> </div>
<div class="span3">name</div> <div class="span2" style="margin: auto; text-align: center;">
<div class="span3">x(mm)</div> <a href='#' style="vertical-align: bottom;" data-bind='click: $parent.moveMacroUp' class="fa fa-chevron-up"></a>
<div class="span3">y(mm)</div> <a href='#' style="vertical-align: bottom;" data-bind='click: $parent.moveMacroDown' class="fa fa-chevron-down"></a>
<div class="span3"> </div> <a href='#' style="vertical-align: bottom;" data-bind='click: $parent.removeMacro' class="fa fa-trash-o"></a>
</div> </div>
</div> </div>
</div> </div>
<div data-bind="foreach: settings.settings.plugins.klipper.probe.points" class="control-group"> <label class="control-label">{{ _('Command') }}</label>
<label class="control-label" data-bind="text: $index"></label> <div class="controls" style="margin-left: 82px;">
<div class="controls"> <div class="row-fluid">
<div class="row-fluid"> <div class="span12" style="margin-top:2px;">
<div class="span3"><input type="text" class="input-block-level" data-bind="value: name"></div> <textarea rows="2" class="block" data-bind="value: macro">
<div class="span3"><input type="text" class="input-block-level" data-bind="value: x"></div> </textarea>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: y"></div> </div>
<div class="span3"> </div>
<a href='#' data-bind='click: $parent.moveProbePointUp' class="fa fa-chevron-up"></a>
<a href='#' data-bind='click: $parent.moveProbePointDown' class="fa fa-chevron-down"></a>
<a href='#' data-bind='click: $parent.removeProbePoint' class="fa fa-trash-o"></a>
</div> </div>
</div> </div>
</div> </div>
<div class="control-group">
<div class="controls">
<a href='#' data-bind='click: addMacro' title="{{ _('Add Macro') }}" class="fa fa-plus-circle"></a> {{ _('Add Macro') }}
</div>
</div>
<div class="control-group">
<span class="help-block">
{{ _('To show a dialog that asks for parameters you can write your macro like in the following example:') }}<br />
</span>
</div>
<div class="control-group">
<pre>
PID_CALIBRATE
HEATER={label:Heater, default:extruder, options:extruder|extruder1}
TARGET={label:Target Temperature, unit:°C, default:190}
WRITE_FILE={label:Write to File, default:0, options:0|1}
</pre>
</div>
</div> </div>
<div class="control-group"> <!-- Leveling -->
<div class="controls"> <div class="tab-pane" id="level">
<a href='#' data-bind="click: addProbePoint" title="{{ _('Add Point') }}" class="fa fa-plus-circle"></a> {{ _('Add Point') }} <div class="control-group">
<span class="help-block">
{{ _('This feature assists in manually leveling you print bed by moving the head to the defined points in
sequence.<br />
If you use a piece of paper for leveling, set "Probe Height" to the paper thickness eg. "0.1".') }}
</span>
</div>
<div class="control-group">
<label class="control-label">{{ _('Probe Height') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.height" />
<span class="add-on">mm</span>
</div>
<span class="help-inline">{{ _('Z-height to probe at') }}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Probe Lift') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.lift" />
<span class="add-on">mm</span>
</div>
<span class="help-inline">{{ _('Lift Head by this amount before moving.') }}</span>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Probe Feedrate Z') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_z" />
<span class="add-on">mm/min</span>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Feedrate X/Y') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level span3" data-bind="value: settings.settings.plugins.klipper.probe.speed_xy" />
<span class="add-on">mm/min</span>
</div>
</div>
</div>
<div class="control-group">
<h5>{{ _('Probe Points') }}</h5>
<div class="controls">
<div class="row-fluid">
<div class="span3">name</div>
<div class="span3">x(mm)</div>
<div class="span3">y(mm)</div>
<div class="span3"> </div>
</div>
</div>
</div>
<div data-bind="foreach: settings.settings.plugins.klipper.probe.points" class="control-group">
<label class="control-label" data-bind="text: $index"></label>
<div class="controls">
<div class="row-fluid">
<div class="span3"><input type="text" class="input-block-level" data-bind="value: name" /></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: x" /></div>
<div class="span3"><input type="text" class="input-block-level" data-bind="value: y" /></div>
<div class="span3">
<a href='#' data-bind='click: $parent.moveProbePointUp' class="fa fa-chevron-up"></a>
<a href='#' data-bind='click: $parent.moveProbePointDown' class="fa fa-chevron-down"></a>
<a href='#' data-bind='click: $parent.removeProbePoint' class="fa fa-trash-o"></a>
</div>
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<a href='#' data-bind="click: addProbePoint" title="{{ _('Add Point') }}" class="fa fa-plus-circle"></a> {{_ ('Add Point') }}
</div>
</div>
</div>
<!-- Klipper Conf -->
<div class="tab-pane" id="conf">
<div class="klipper-column-fluid klipper-settings-tab">
<h3 class="text-center m-0">{{ _('Config Files') }}</h3>
<div class="klipper-row-fluid">
<div class="klipper-fluid-item-2" data-bind="visible: $root.klipperViewModel.hasRightKo('CONFIG')">
<button class="btn btn-small" data-bind="click: newFile" title="{{ _('Add new File') }}">
<i class="far fa-file"></i> {{ _('New File') }}
</button>
<button class="btn btn-small" data-bind="click: listCfgFiles" title="{{ _('Refresh file list') }}">
<i class="icon-refresh"></i> {{ _('Refresh Files') }}
</button>
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="far fa-square"></i>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="javascript:void(0)" data-bind="click: markFilesOnPage">{{ _('Select all on this page') }}</a></li>
<li><a href="javascript:void(0)" data-bind="click: markAllFiles">{{ _('Select all') }}</a></li>
<li class="divider"></li>
<li><a href="javascript:void(0)" data-bind="click: clearMarkedFiles">{{ _('Clear selection') }}</a></li>
</ul>
</div>
<button class="btn btn-small"
data-bind="click: removeMarkedFiles, enable: markedForFileRemove().length > 0">{{ _('Delete selected') }}</button>
</div>
<div class="pull-right">
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" data-bind="click: function() { configs.changeSorting('name'); }"><i class="fas fa-check"
data-bind="style: {visibility: configs.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }}
({{ _('ascending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { configs.changeSorting('date'); }"><i class="fas fa-check"
data-bind="style: {visibility: configs.currentSorting() == 'date' ? 'visible' : 'hidden'}"></i> {{ _('Sort by date') }}
({{ _('descending') }})</a></li>
<li><a href="javascript:void(0)" data-bind="click: function() { configs.changeSorting('size'); }"><i class="fas fa-check"
data-bind="style: {visibility: configs.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }}
({{ _('descending') }})</a></li>
</ul>
</div>
</div>
</div>
<div class="scroll-y">
<table class="table table-striped table-hover table-condensed table-hover table-fixed" id="klipper_cfg_files">
<thead>
<tr>
<th class="klipper_cfgs_checkbox span1"></th>
<th class="klipper_cfgs_name">{{ _('Name') }}</th>
<th class="klipper_cfgs_size">{{ _('Size') }}</th>
<th class="klipper_cfgs_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: configs.paginatedItems">
<tr data-bind="attr: {title: name}">
<td class="klipper_cfgs_checkbox">
<input type="checkbox"
data-bind="value: name, checked: $root.markedForFileRemove, invisible: !$root.klipperViewModel.hasRightKo('CONFIG')">
</td>
<td class="klipper_cfgs_name" data-bind="text: name"></td>
<td class="klipper_cfgs_size" data-bind="text: size"></td>
<td class="klipper_cfgs_action">
<a href="javascript:void(0)" class="far fa-trash-alt" title="{{ _('Delete') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.removeCfg($data.name);}"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-download" title="{{ _('Download') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, attr: { href: ($root.klipperViewModel.hasRightKo('CONFIG')()) ? $data.url : 'javascript:void(0)'}"></a>
&nbsp;|&nbsp;
<a href="javascript:void(0)" class="fas fa-pencil-alt" title="{{ _('Edit') }}"
data-bind="css: {disabled: !$root.klipperViewModel.hasRightKo('CONFIG')()}, click: function() { $parent.showEditUserDialog($data.name);}"></a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: configs.currentPage() === 0}"><a href="javascript:void(0)" data-bind="click: configs.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: configs.pages">
<li data-bind="css: {active: $data.number === $root.configs.currentPage(), disabled: $data.number === -1 }">
<a href="javascript:void(0)" data-bind="text: $data.text, click: function() { $root.configs.changePage($data.number); }"></a>
</li>
</ul>
<ul>
<li data-bind="css: {disabled: configs.currentPage() === configs.lastPage()}">
<a href="javascript:void(0)"
data-bind="click: configs.nextPage">»
</a>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Klipper Conf --> <div class="tab-footer">
<div class="tab-pane" id="conf"> <a href="https://www.paypal.com/donate/?business=7P63W664NF8LA&item_name=OctoKlipper" class="btn btn-mini" target="_blank">
<div class="control-group"> <i class="fab fa-paypal"> {{ _('Donate') }}</i>
<script src="plugin/klipper/static/js/lib/ace/ace.min.js" type="text/javascript" charset="utf-8"></script> </a>
<script src="plugin/klipper/static/js/lib/ace/theme-monokai.min.js" type="text/javascript" charset="utf-8"></script>
<script src="plugin/klipper/static/js/lib/ace/mode-klipper_config.js" type="text/javascript"></script>
<div class="editor-controls">
<button class="btn btn-small" data-bind="click: loadCfgBackup"
title="{{ _('Reload last version') }}">
<i class="fas fa-redo"></i> {{ _('Reload last version') }}
</button>
<button class="btn btn-small" data-bind='click: reloadFromFile'
title="{{ _('Reload from file') }}">
<i class="fas fa-upload"></i> {{ _('Reload from file') }}
</button>
<label class="inline"><input class="inline-checkbox" type="checkbox" data-bind="checked: settings.settings.plugins.klipper.configuration.parse_check"> {{ _('Check parsing on save') }}</label>
&nbsp;&nbsp;
<a href='#' data-bind="click: minusFontsize" title="{{ _('Decrease Fontsize') }}" class="fas fa-search-minus"></a>
<a href='#' data-bind="click: plusFontsize" title="{{ _('Increase Fontsize') }}" class="fas fa-search-plus"></a>
</div>
<div class="conf-editor">
<input id="hdnLoadKlipperConfig" type="hidden" data-bind="value: configBound(settings.settings.plugins.klipper.config)" />
<div id="plugin-klipper-config"></div>
</div>
</div>
</div> </div>
</div>
<div class="tab-footer">
<a href="https://www.paypal.com/donate/?business=7P63W664NF8LA&item_name=OctoKlipper" class="btn btn-mini" target="_blank">
<i class="fab fa-paypal"> {{ _('Donate') }}</i>
</a>
</div>
</form> </form>

View File

@ -3,7 +3,7 @@
<label for="connection_printers" data-bind="css: {disabled: !connectionState.isErrorOrClosed()}, enable: connectionState.isErrorOrClosed() && loginState.isUser()">{{ _('Printer Profile') }}</label> <label for="connection_printers" data-bind="css: {disabled: !connectionState.isErrorOrClosed()}, enable: connectionState.isErrorOrClosed() && loginState.isUser()">{{ _('Printer Profile') }}</label>
<select id="connection_printers" data-bind="options: connectionState.printerOptions, optionsText: 'name', optionsValue: 'id', value: connectionState.selectedPrinter, css: {disabled: !connectionState.isErrorOrClosed()}, enable: connectionState.isErrorOrClosed() && loginState.isUser()"></select> <select id="connection_printers" data-bind="options: connectionState.printerOptions, optionsText: 'name', optionsValue: 'id', value: connectionState.selectedPrinter, css: {disabled: !connectionState.isErrorOrClosed()}, enable: connectionState.isErrorOrClosed() && loginState.isUser()"></select>
<button class="btn btn-block" data-bind="click: connectionState.connect, text: connectionState.buttonText(), enable: loginState.isUser()">{{ _('Connect') }}</button> <button class="btn btn-block" data-bind="click: connectionState.connect, text: connectionState.buttonText(), enable: loginState.isUser()">{{ _('Connect') }}</button>
<button class="btn btn-block" data-bind="visible: hasRight('CONFIG', 'Ko'), click: function() {openOctoKlipperSettings('klipper-config');}">{{ _('Open Klipper config') }}</button> <button class="btn btn-block" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_CONFIG), click: function() {openOctoKlipperSettings('klipper-config');}">{{ _('Open Klipper config') }}</button>
</div> </div>
</div> </div>
<!-- ko if: settings.settings.plugins.klipper.configuration.shortStatus_sidebar --> <!-- ko if: settings.settings.plugins.klipper.configuration.shortStatus_sidebar -->
@ -11,7 +11,7 @@
<a title="{{ _('Go to OctoKlipper Tab') }}" data-bind="text: shortStatus_sidebar, click: navbarClicked"></a> <a title="{{ _('Go to OctoKlipper Tab') }}" data-bind="text: shortStatus_sidebar, click: navbarClicked"></a>
</div> </div>
<!-- /ko --> <!-- /ko -->
<div class="control-group" data-bind="visible: hasRight('MACRO', 'Ko')"> <div class="control-group" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_MACRO)">
<div class="controls"> <div class="controls">
<label class="control-label small"><i class="icon-list-alt"></i> {{ _('Macros') }}</label> <label class="control-label small"><i class="icon-list-alt"></i> {{ _('Macros') }}</label>
<div data-bind="foreach: settings.settings.plugins.klipper.macros"> <div data-bind="foreach: settings.settings.plugins.klipper.macros">

View File

@ -8,11 +8,7 @@
</div> </div>
</div> </div>
&nbsp; &nbsp;
<button <button class="btn btn-mini pull-right clear-btn" data-bind="click: onClearLog" title="{{ _('Clear Log') }}">
class="btn btn-mini pull-right clear-btn"
data-bind="click: onClearLog"
title="{{ _('Clear Log') }}"
>
<i class="fa fa-trash"></i> {{ _('Clear Log') }} <i class="fa fa-trash"></i> {{ _('Clear Log') }}
</button> </button>
</div> </div>
@ -21,18 +17,13 @@
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="control-label"></label> <label class="control-label"></label>
<button <button class="btn btn-block btn-small" data-bind="click: onGetStatus, enable: isActive()"
class="btn btn-block btn-small" title="{{ _('Query Klipper for its current status') }}">
data-bind="click: onGetStatus, enable: isActive()"
title="{{ _('Query Klipper for its current status') }}"
>
<i class="fa icon-black fa-info-circle"></i> {{ _("Get Status") }} <i class="fa icon-black fa-info-circle"></i> {{ _("Get Status") }}
</button> </button>
<button <button class="btn btn-block btn-small"
class="btn btn-block btn-small" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_CONFIG), click: function() {openOctoKlipperSettings('klipper-config');}"
data-bind="visible: hasRight('CONFIG', 'Ko'), click: function() {openOctoKlipperSettings('klipper-config');}" title="{{ _('Open the Klipper configuration file') }}">
title="{{ _('Open the Klipper configuration file') }}"
>
<i class="fa icon-black fa-file-code-o"></i> <i class="fa icon-black fa-file-code-o"></i>
{{ _("Open Klipper config") }} {{ _("Open Klipper config") }}
</button> </button>
@ -40,70 +31,44 @@
</div> </div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="control-label small" <label class="control-label small"><i class="icon-refresh"></i> {{ _("Restart") }}</label>
><i class="icon-refresh"></i> {{ _("Restart") }}</label <button class="btn btn-block btn-small" data-bind="click: onRestartHost, enable: isActive()"
> title="{{ _('This will cause the host software to reload its config and perform an internal reset') }}">
<button
class="btn btn-block btn-small"
data-bind="click: onRestartHost, enable: isActive()"
title="{{ _('This will cause the host software to reload its config and perform an internal reset') }}"
>
{{ _("Host") }} {{ _("Host") }}
</button> </button>
<button <button class="btn btn-block btn-small" data-bind="click: onRestartFirmware, enable: isActive()"
class="btn btn-block btn-small" title="{{ _('Similar to a host restart, but also clears any error state from the micro-controller') }}">
data-bind="click: onRestartFirmware, enable: isActive()"
title="{{ _('Similar to a host restart, but also clears any error state from the micro-controller') }}"
>
{{ _("Firmware") }} {{ _("Firmware") }}
</button> </button>
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<label class="control-label" <label class="control-label"><i class="icon-wrench"></i> {{ _("Tools") }}</label>
><i class="icon-wrench"></i> {{ _("Tools") }}</label <button class="btn btn-block btn-small" data-bind="click: showLevelingDialog, enable: isActive()"
> title="{{ _('Assists in manually leveling your printbed by moving the head to a configurable set of positions in sequence.') }}">
<button
class="btn btn-block btn-small"
data-bind="click: showLevelingDialog, enable: isActive()"
title="{{ _('Assists in manually leveling your printbed by moving the head to a configurable set of positions in sequence.') }}"
>
{{ _("Assisted Bed Leveling") }} {{ _("Assisted Bed Leveling") }}
</button> </button>
<button <button class="btn btn-block btn-small" data-bind="click: showPidTuningDialog, enable: isActive()"
class="btn btn-block btn-small" title="{{ _('Determines optimal PID parameters by heat cycling the hotend/bed.') }}">
data-bind="click: showPidTuningDialog, enable: isActive()"
title="{{ _('Determines optimal PID parameters by heat cycling the hotend/bed.') }}"
>
{{ _("PID Tuning") }} {{ _("PID Tuning") }}
</button> </button>
<button <button class="btn btn-block btn-small" data-bind="click: showOffsetDialog, enable: isActive()"
class="btn btn-block btn-small" title="{{ _('Sets a offset for subsequent GCODE coordinates.') }}">
data-bind="click: showOffsetDialog, enable: isActive()"
title="{{ _('Sets a offset for subsequent GCODE coordinates.') }}"
>
{{ _("Coordinate Offset") }} {{ _("Coordinate Offset") }}
</button> </button>
<button <button class="btn btn-block btn-small" data-bind="click: showGraphDialog"
class="btn btn-block btn-small" title="{{ _('Assists in debugging performance issues by analyzing the Klipper log files.') }}">
data-bind="click: showGraphDialog"
title="{{ _('Assists in debugging performance issues by analyzing the Klipper log files.') }}"
>
{{ _("Analyze Klipper Log") }} {{ _("Analyze Klipper Log") }}
</button> </button>
</div> </div>
</div> </div>
<div class="controls" data-bind="visible: hasRight('MACRO', 'Ko')"> <div class="controls" data-bind="visible: $root.loginState.hasPermissionKo($root.access.permissions.PLUGIN_KLIPPER_MACRO)">
<label class="control-label" <label class="control-label"><i class="icon-list-alt"></i> {{ _("Macros") }}</label>
><i class="icon-list-alt"></i> {{ _("Macros") }}</label
>
<div data-bind="foreach: settings.settings.plugins.klipper.macros"> <div data-bind="foreach: settings.settings.plugins.klipper.macros">
<!-- ko if: tab --> <!-- ko if: tab -->
<button <button class="btn btn-block btn-small" data-bind="text: name, click: $parent.executeMacro, enable: $parent.isActive()">
class="btn btn-block btn-small" </button>
data-bind="text: name, click: $parent.executeMacro, enable: $parent.isActive()"
></button>
<!-- /ko --> <!-- /ko -->
</div> </div>
</div> </div>

41
octoprint_klipper/util.py Normal file
View File

@ -0,0 +1,41 @@
def poll_status(self):
self._printer.commands("STATUS")
def update_status(self, type, status):
send_message(self, "status", type, status, status)
def file_exist(self, filepath):
'''
Returns if a file exists and shows PopUp if not
'''
from os import path
if not path.isfile(filepath):
send_message(self, "PopUp", "warning", "OctoKlipper Settings",
"File: <br />" + filepath + "<br /> does not exist!")
return False
else:
return True
def key_exist(dict, key1, key2):
try:
dict[key1][key2]
except KeyError:
return False
else:
return True
def send_message(self, type, subtype, title, payload):
"""
Send Message over API to FrontEnd
"""
import datetime
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
)
)