+reload configfile as the settings dialog is opened

+Syntax Highlighting for Configfile
+Show logfile content on the Performance Graph Dialog
+Check parsing of configfile before saving
This commit is contained in:
thelastWallE 2021-03-26 23:39:08 +01:00
parent 19f2df12ee
commit 415da30ddf
11 changed files with 1275 additions and 561 deletions

View File

@ -12,7 +12,7 @@ indent_style = space
max_line_length = 90
[**.py]
indent_size = 3
indent_size = 4
[**.yaml]
indent_size = 2

View File

@ -13,6 +13,7 @@
# 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/>.
from __future__ import absolute_import, division, print_function, unicode_literals
import datetime
import logging
import octoprint.plugin
@ -24,429 +25,516 @@ from octoprint.util.comm import parse_firmware_line
from octoprint.access.permissions import Permissions, ADMIN_GROUP, USER_GROUP
from .modules import KlipperLogAnalyzer
import flask
import configparser
from flask_babel import gettext
try:
import configparser
except ImportError:
import ConfigParser as configparser
class KlipperPlugin(
octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin,
octoprint.plugin.AssetPlugin,
octoprint.plugin.SimpleApiPlugin,
octoprint.plugin.EventHandlerPlugin):
octoprint.plugin.StartupPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SettingsPlugin,
octoprint.plugin.AssetPlugin,
octoprint.plugin.SimpleApiPlugin,
octoprint.plugin.EventHandlerPlugin):
_parsing_response = False
_message = ""
_parsing_response = False
_message = ""
#-- Startup Plugin
# -- Startup Plugin
def on_after_startup(self):
klipper_port = self._settings.get(["connection", "port"])
additional_ports = self._settings.global_get(["serial", "additionalPorts"])
def on_after_startup(self):
klipper_port = self._settings.get(["connection", "port"])
additional_ports = self._settings.global_get(
["serial", "additionalPorts"])
if klipper_port not in additional_ports:
additional_ports.append(klipper_port)
self._settings.global_set(["serial", "additionalPorts"], additional_ports)
self._settings.save()
self._logger.info("Added klipper serial port {} to list of additional ports.".format(klipper_port))
if klipper_port not in additional_ports:
additional_ports.append(klipper_port)
self._settings.global_set(
["serial", "additionalPorts"], additional_ports)
self._settings.save()
self._logger.info(
"Added klipper serial port {} to list of additional ports.".format(klipper_port))
#-- Settings Plugin
# -- Settings Plugin
def get_additional_permissions(self, *args, **kwargs):
def get_additional_permissions(self, *args, **kwargs):
return [
{
"key": "CONFIG",
"name": "Config Klipper",
"description": gettext("Allows to config klipper"),
"default_groups": [ADMIN_GROUP],
"dangerous": True,
"roles": ["admin"]
{
"key": "CONFIG",
"name": "Config Klipper",
"description": gettext("Allows to config klipper"),
"default_groups": [ADMIN_GROUP],
"dangerous": True,
"roles": ["admin"]
},
{
"key": "MACRO",
"name": "Use Klipper Macros",
"description": gettext("Allows to use klipper macros"),
"default_groups": [ADMIN_GROUP],
"dangerous": True,
"roles": ["admin"]
"key": "MACRO",
"name": "Use Klipper Macros",
"description": gettext("Allows to use klipper macros"),
"default_groups": [ADMIN_GROUP],
"dangerous": True,
"roles": ["admin"]
},
]
def get_settings_defaults(self):
return dict(
connection = dict(
port="/tmp/printer",
replace_connection_panel=True
),
macros = [dict(
name="E-Stop",
macro="M112",
sidebar=True,
tab=True
)],
probe = dict(
height=0,
lift=5,
speed_xy=1500,
speed_z=500,
points=[dict(
name="point-1",
x=0,
y=0
)]
),
configuration = dict(
configpath="~/printer.cfg",
logpath="/tmp/klippy.log",
reload_command="RESTART",
navbar=True
)
)
def on_settings_load(self):
data = octoprint.plugin.SettingsPlugin.on_settings_load(self)
configpath = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
logpath = os.path.expanduser(
self._settings.get(["configuration", "logpath"])
)
try:
f = open(configpath, "r")
data["config"] = f.read()
f.close()
except IOError:
self._logger.error(
"Error: Klipper config file not found at: {}".format(configpath)
)
return data
def reloadConfigfile(self):
data = octoprint.plugin.SettingsPlugin.on_settings_load(self)
filepath = os.path.expanduser(
self._settings.get(["configuration", "configpath"]))
try:
f = open(filepath, "r", encoding="utf-8")
data["config"] = f.read()
f.close()
except IOError:
self._logger.error(
"Error: Klipper config file not found at: {}".format(filepath))
return data
def on_settings_save(self, data):
if self.keyexists(data, "configuration", "configpath"):
configpath = os.path.expanduser(
data["configuration"]["configpath"]
)
self.checkFile("config",configpath)
if self.keyexists(data, "configuration", "logpath"):
logpath = os.path.expanduser(
data["configuration"]["logpath"]
)
self.checkFile("log",logpath)
if "config" in data:
try:
data["config"] = data["config"].encode('utf-8')
# Basic validation of config file - makes sure it parses
try:
parser = configparser.RawConfigParser()
parser.read_string(data["config"])
except configParser.Error as error:
self._logger.error(
"Error: Invalid Klipper config file: {}".format(str(error))
)
self.sendMessage("errorPopUp","warning", "OctoKlipper Settings", "Invalid Klipper config file: " + str(error))
f = open(configpath, "w")
f.write(data["config"])
f.close()
self._logger.error(
"Writing Klipper config to {}".format(configpath)
)
# Restart klipply to reload config
self._printer.commands(self._settings.get(["configuration", "reload_command"]))
self.logInfo("Reloading Klipper Configuration.")
except IOError:
self._logger.error(
"Error: Couldn't write Klipper config file: {}".format(configpath)
)
data.pop("config", None) # we dont want to write the klipper conf to the octoprint settings
else:
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
def get_settings_restricted_paths(self):
return dict(
admin=[
["connection", "port"],
["configuration", "configpath"],
["configuration", "replace_connection_panel"]
],
user=[
["macros"],
["probe"]
]
)
def get_settings_version(self):
return 2
def on_settings_migrate(self, target, current):
if current is None:
settings = self._settings
if settings.has(["serialport"]):
settings.set(["connection", "port"], settings.get(["serialport"]) )
settings.remove(["serialport"])
if settings.has(["replace_connection_panel"]):
settings.set(
["connection", "replace_connection_panel"],
settings.get(["replace_connection_panel"])
)
settings.remove(["replace_connection_panel"])
if settings.has(["probeHeight"]):
settings.set(["probe", "height"], settings.get(["probeHeight"]))
settings.remove(["probeHeight"])
if settings.has(["probeLift"]):
settings.set(["probe", "lift"], settings.get(["probeLift"]))
settings.remove(["probeLift"])
if settings.has(["probeSpeedXy"]):
settings.set(["probe", "speed_xy"], settings.get(["probeSpeedXy"]))
settings.remove(["probeSpeedXy"])
if settings.has(["probeSpeedZ"]):
settings.set(["probe", "speed_z"], settings.get(["probeSpeedZ"]))
settings.remove(["probeSpeedZ"])
if settings.has(["probePoints"]):
points = settings.get(["probePoints"])
points_new = []
for p in points:
points_new.append(dict(name="", x=int(p["x"]), y=int(p["y"]), z=0))
settings.set(["probe", "points"], points_new)
settings.remove(["probePoints"])
if settings.has(["configPath"]):
settings.set(["config_path"], settings.get(["configPath"]))
settings.remove(["configPath"])
#-- Template Plugin
def get_template_configs(self):
return [
dict(type="navbar", custom_bindings=True),
dict(type="settings", custom_bindings=True),
dict(
type="generic",
name="Assisted Bed Leveling",
template="klipper_leveling_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="PID Tuning",
template="klipper_pid_tuning_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="Coordinate Offset",
template="klipper_offset_dialog.jinja2",
custom_bindings=True
),
dict(
type="tab",
name="Klipper",
template="klipper_tab_main.jinja2",
suffix="_main",
custom_bindings=True
),
dict(type="sidebar",
custom_bindings=True,
icon="rocket",
replaces= "connection" if self._settings.get_boolean(["connection", "replace_connection_panel"]) else ""
),
dict(
type="generic",
name="Performance Graph",
template="klipper_graph_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="Macro Dialog",
template="klipper_param_macro_dialog.jinja2",
custom_bindings=True
)
]
#-- Asset Plugin
def get_assets(self):
return dict(
js=["js/klipper.js",
"js/klipper_settings.js",
"js/klipper_leveling.js",
"js/klipper_pid_tuning.js",
"js/klipper_offset.js",
"js/klipper_param_macro.js",
"js/klipper_graph.js"
],
css=["css/klipper.css"],
less=["css/klipper.less"]
)
#-- Event Handler Plugin
def on_event(self, event, payload):
if "UserLoggedIn" == event:
self.updateStatus("info","Klipper: Standby")
if "Connecting" == event:
self.updateStatus("info", "Klipper: Connecting ...")
elif "Connected" == event:
self.updateStatus("info", "Klipper: Connected to host")
self.logInfo("Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"]))
elif "Disconnected" == event:
self.updateStatus("info", "Klipper: Disconnected from host")
elif "Error" == event:
self.updateStatus("error", "Klipper: Error")
self.logError(payload["error"])
#-- GCODE Hook
def on_parse_gcode(self, comm, line, *args, **kwargs):
if "FIRMWARE_VERSION" in line:
printerInfo = parse_firmware_line(line)
if "FIRMWARE_VERSION" in printerInfo:
self.logInfo("Firmware version: {}".format(printerInfo["FIRMWARE_VERSION"]))
elif "//" in line:
self._message = self._message + line.strip('/')
if not self._parsing_response:
self.updateStatus("info", self._message)
self._parsing_response = True
else:
if self._parsing_response:
self._parsing_response = False
self.logInfo(self._message)
self._message = ""
if "!!" in line:
msg = line.strip('!')
self.updateStatus("error", msg)
self.logError(msg)
return line
def get_api_commands(self):
return dict(
listLogFiles=[],
getStats=["logFile"],
loadConfig=["configFile"]
)
def on_api_command(self, command, data):
if command == "listLogFiles":
files = []
for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"):
filesize = os.path.getsize(f)
files.append(dict(
name=os.path.basename(f) + " ({:.1f} KB)".format(filesize / 1000.0),
file=f,
size=filesize
))
return flask.jsonify(data=files)
elif command == "getStats":
if "logFile" in data:
log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer(data["logFile"])
return flask.jsonify(log_analyzer.analyze())
elif command == "loadConfig":
kc = Parser()
sections = kc.load(data["configFile"])
return flask.jsonify(sections)
def get_update_information(self):
return dict(
klipper=dict(
displayName=self._plugin_name,
displayVersion=self._plugin_version,
type="github_release",
current=self._plugin_version,
user="thelastWallE",
repo="OctoprintKlipperPlugin",
pip="https://github.com/thelastWallE/OctoprintKlipperPlugin/archive/{target_version}.zip",
stable_branch=dict(
name="Stable",
branch="master",
comittish=["master"]
def get_settings_defaults(self):
return dict(
connection=dict(
port="/tmp/printer",
replace_connection_panel=True
),
prerelease_branches=[
dict(
name="Release Candidate",
branch="rc",
comittish=["rc", "master"],
)
macros=[dict(
name="E-Stop",
macro="M112",
sidebar=True,
tab=True
)],
probe=dict(
height=0,
lift=5,
speed_xy=1500,
speed_z=500,
points=[dict(
name="point-1",
x=0,
y=0
)]
),
configuration=dict(
configpath="~/printer.cfg",
logpath="/tmp/klippy.log",
reload_command="RESTART",
navbar=True
)
)
def on_settings_load(self):
data = octoprint.plugin.SettingsPlugin.on_settings_load(self)
configpath = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
try:
f = open(configpath, "r")
data["config"] = f.read()
f.close()
except IOError:
self._logger.error(
"Error: Klipper config file not found at: {}".format(
configpath)
)
else:
self.send_message("reload", "config", "", data["config"])
# send the configdata to frontend to update ace editor
return data
def on_settings_save(self, data):
self.log_console(
"debug",
"Save klipper configs"
)
if "config" in data:
try:
if sys.version_info[0] < 3:
data["config"] = data["config"].encode('utf-8')
# check for configpath if it was changed during changing of the configfile
if self.key_exist(data, "configuration", "configpath"):
configpath = os.path.expanduser(
data["configuration"]["configpath"]
)
else:
# if the configpath was not changed during changing the printer.cfg. Then the configpath would not be in data[]
configpath = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
if self.file_exist(configpath) and self.validate_configfile(data["config"]):
f = open(configpath, "w")
f.write(data["config"])
f.close()
# Restart klippy to reload config
self._printer.commands(self._settings.get(
["configuration", "reload_command"]))
self.log_info("Reloading Klipper Configuration.")
self.log_console(
"debug",
("Writing Klipper config to {}".format(configpath))
)
except IOError:
self._logger.error(
"Error: Couldn't write Klipper config file: {}".format(
configpath)
)
else:
# we dont want to write the klipper conf to the octoprint settings
data.pop("config", None)
# save the rest of changed settings into config.yaml of octoprint
octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
def get_settings_restricted_paths(self):
return dict(
admin=[
["connection", "port"],
["configuration", "configpath"],
["configuration", "replace_connection_panel"]
],
user=[
["macros"],
["probe"]
]
)
)
)
#-- Helpers
def sendMessage(self, type, subtype, title, payload):
self._plugin_manager.send_plugin_message(
self._identifier,
dict(
time=datetime.datetime.now().strftime("%H:%M:%S"),
type=type,
subtype=subtype,
title=title,
payload=payload
)
)
def get_settings_version(self):
return 2
def pollStatus(self):
self._printer.commands("STATUS")
def on_settings_migrate(self, target, current):
if current is None:
settings = self._settings
def updateStatus(self, type, status):
self.sendMessage("status", type, status, status)
if settings.has(["serialport"]):
settings.set(["connection", "port"],
settings.get(["serialport"]))
settings.remove(["serialport"])
def logInfo(self, message):
self.sendMessage("log", "info", message, message)
if settings.has(["replace_connection_panel"]):
settings.set(
["connection", "replace_connection_panel"],
settings.get(["replace_connection_panel"])
)
settings.remove(["replace_connection_panel"])
def logError(self, error):
self.sendMessage("log", "error", error, error)
if settings.has(["probeHeight"]):
settings.set(["probe", "height"],
settings.get(["probeHeight"]))
settings.remove(["probeHeight"])
if settings.has(["probeLift"]):
settings.set(["probe", "lift"], settings.get(["probeLift"]))
settings.remove(["probeLift"])
if settings.has(["probeSpeedXy"]):
settings.set(["probe", "speed_xy"],
settings.get(["probeSpeedXy"]))
settings.remove(["probeSpeedXy"])
if settings.has(["probeSpeedZ"]):
settings.set(["probe", "speed_z"],
settings.get(["probeSpeedZ"]))
settings.remove(["probeSpeedZ"])
if settings.has(["probePoints"]):
points = settings.get(["probePoints"])
points_new = []
for p in points:
points_new.append(
dict(name="", x=int(p["x"]), y=int(p["y"]), z=0))
settings.set(["probe", "points"], points_new)
settings.remove(["probePoints"])
if settings.has(["configPath"]):
settings.set(["config_path"], settings.get(["configPath"]))
settings.remove(["configPath"])
# -- Template Plugin
def get_template_configs(self):
return [
dict(type="navbar", custom_bindings=True),
dict(type="settings", custom_bindings=True),
dict(
type="generic",
name="Assisted Bed Leveling",
template="klipper_leveling_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="PID Tuning",
template="klipper_pid_tuning_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="Coordinate Offset",
template="klipper_offset_dialog.jinja2",
custom_bindings=True
),
dict(
type="tab",
name="Klipper",
template="klipper_tab_main.jinja2",
suffix="_main",
custom_bindings=True
),
dict(type="sidebar",
custom_bindings=True,
icon="rocket",
replaces="connection" if self._settings.get_boolean(
["connection", "replace_connection_panel"]) else ""
),
dict(
type="generic",
name="Performance Graph",
template="klipper_graph_dialog.jinja2",
custom_bindings=True
),
dict(
type="generic",
name="Macro Dialog",
template="klipper_param_macro_dialog.jinja2",
custom_bindings=True
)
]
# -- Asset Plugin
def get_assets(self):
return dict(
js=["js/klipper.js",
"js/klipper_settings.js",
"js/klipper_leveling.js",
"js/klipper_pid_tuning.js",
"js/klipper_offset.js",
"js/klipper_param_macro.js",
"js/klipper_graph.js"
],
css=["css/klipper.css"],
less=["css/klipper.less"]
)
# -- Event Handler Plugin
def on_event(self, event, payload):
if "UserLoggedIn" == event:
self.update_status("info", "Klipper: Standby")
if "Connecting" == event:
self.update_status("info", "Klipper: Connecting ...")
elif "Connected" == event:
self.update_status("info", "Klipper: Connected to host")
self.log_info(
"Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"]))
elif "Disconnected" == event:
self.update_status("info", "Klipper: Disconnected from host")
elif "Error" == event:
self.update_status("error", "Klipper: Error")
self.log_error(payload["error"])
# -- GCODE Hook
def on_parse_gcode(self, comm, line, *args, **kwargs):
if "FIRMWARE_VERSION" in line:
printerInfo = parse_firmware_line(line)
if "FIRMWARE_VERSION" in printerInfo:
self.log_info("Firmware version: {}".format(
printerInfo["FIRMWARE_VERSION"]))
elif "//" in line:
self._message = self._message + line.strip('/')
if not self._parsing_response:
self.update_status("info", self._message)
self._parsing_response = True
else:
if self._parsing_response:
self._parsing_response = False
self.log_info(self._message)
self._message = ""
if "!!" in line:
msg = line.strip('!')
self.update_status("error", msg)
self.log_error(msg)
return line
def get_api_commands(self):
return dict(
listLogFiles=[],
getStats=["logFile"],
reloadConfig=[]
)
def on_api_command(self, command, data):
if command == "listLogFiles":
files = []
logpath = os.path.expanduser(
self._settings.get(["configuration", "logpath"])
)
if self.file_exist(logpath):
for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"):
filesize = os.path.getsize(f)
files.append(dict(
name=os.path.basename(
f) + " ({:.1f} KB)".format(filesize / 1000.0),
file=f,
size=filesize
))
return flask.jsonify(data=files)
else:
return flask.jsonify(data=files)
elif command == "getStats":
if "logFile" in data:
log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer(
data["logFile"])
return flask.jsonify(log_analyzer.analyze())
elif command == "reloadConfig":
data = octoprint.plugin.SettingsPlugin.on_settings_load(self)
configpath = os.path.expanduser(
self._settings.get(["configuration", "configpath"])
)
try:
f = open(configpath, "r")
data["config"] = f.read()
f.close()
except IOError:
self._logger.error(
"Error: Klipper config file not found at: {}".format(
configpath)
)
else:
self._settings.set(["config"], data["config"])
self.send_message("reload", "config", "", data["config"])
# send the configdata to frontend to update ace editor
return
def get_update_information(self):
return dict(
klipper=dict(
displayName=self._plugin_name,
displayVersion=self._plugin_version,
type="github_release",
current=self._plugin_version,
user="thelastWallE",
repo="OctoprintKlipperPlugin",
pip="https://github.com/thelastWallE/OctoprintKlipperPlugin/archive/{target_version}.zip",
stable_branch=dict(
name="Stable",
branch="master",
comittish=["master"]
),
prerelease_branches=[
dict(
name="Release Candidate",
branch="rc",
comittish=["rc", "master"]
)]
)
)
#-- Helpers
def send_message(self, type, subtype, title, payload):
self._plugin_manager.send_plugin_message(
self._identifier,
dict(
time=datetime.datetime.now().strftime("%H:%M:%S"),
type=type,
subtype=subtype,
title=title,
payload=payload
)
)
def poll_status(self):
self._printer.commands("STATUS")
def update_status(self, type, status):
self.send_message("status", type, status, status)
def log_info(self, message):
self.send_message("log", "info", message, message)
def log_console(self, consoletype, message):
# sends a message to frontend(in klipper.js -> self.onDataUpdaterPluginMessage) and write it to the console.
# _mtype, subtype=debug/info, title of message, message)
self.send_message("console", consoletype, message, message)
def log_error(self, error):
self.send_message("log", "error", error, error)
def file_exist(self, filepath):
if not os.path.isfile(filepath):
self.send_message("errorPopUp", "warning", "OctoKlipper Settings",
"Klipper " + filepath + " does not exist!")
return False
else:
return True
def key_exist(self, dict, key1, key2):
try:
dict[key1][key2]
except KeyError:
return False
else:
return True
def validate_configfile(self, dataToBeValidated):
"""
--->For now this just checks if the given data can be parsed<----
From https://www.opensourceforu.com/2015/03/practical-python-programming-writing-a-config-file-checker/
Validates a given Config File in filetobevalidated against a correct config file pointed to by goldenfilepath
returns a list of erroneous lines as a list[strings]
if config file is fine, it should return an empty list
"""
#len(ValidateFile('c:\example.cfg', 'c:\example.cfg' ))== 0
# learn golden file
#__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
#goldenfilepath = os.path.join(__location__, "goldenprinter.cfg")
#goldenconfig = ConfigParser.ConfigParser()
# dataToValidated.read(goldenfilepath)
# learn file to be validated
try:
dataToValidated = configparser.RawConfigParser()
dataToValidated.read_string(dataToBeValidated)
except dataToValidated.Error as error:
self._logger.error(
"Error: Invalid Klipper config file: {}".format(str(error))
)
self.send_message("errorPopUp", "warning", "OctoKlipper Settings",
"Invalid Klipper config file: " + str(error))
return "False"
else:
return "True"
#incorrectlines = []
# for section in dataToValidated.sections():
# #check each key is present in corresponding golden section
# for key in dataToValidated.options(section):
# if not goldenconfig.has_option(section,key):
# incorrectlines.append(key + "=" + dataToValidated.get(section,key))
# # print incorrect lines
# if len(incorrectlines) > 0 :
# self.send_message("errorPopUp","warning", "OctoKlipper Settings", "Invalid Klipper config file: " + str(incorrectlines))
# for k in incorrectlines:
# print k
# self._logger.error(
# "Error: Invalid Klipper config line: {}".format(str(k))
# )
# return incorrectlines
def checkFile(self,filetype,filepath):
if not os.path.isfile(filepath):
self.sendMessage("errorPopUp","warning", "OctoKlipper Settings", "Klipper " + filepath + " does not exist!")
def keyexists(self, dict, key1, key2):
try:
dict[key1][key2]
except KeyError:
return False
else:
return True
__plugin_name__ = "OctoKlipper"
__plugin_pythoncompat__ = ">=2.7,<4"
def __plugin_load__():
global __plugin_implementation__
global __plugin_hooks__
__plugin_implementation__ = KlipperPlugin()
__plugin_hooks__ = {
"octoprint.access.permissions": __plugin_implementation__.get_additional_permissions,
"octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode,
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information
}
global __plugin_implementation__
global __plugin_hooks__
__plugin_implementation__ = KlipperPlugin()
__plugin_hooks__ = {
"octoprint.access.permissions": __plugin_implementation__.get_additional_permissions,
"octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode,
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information
}

View File

@ -0,0 +1,519 @@
# Golden printer.cfg to check against. This should contain all settings under each section that are available.
[stepper_x]
step_pin:
# Step GPIO pin (triggered high). This parameter must be provided.
dir_pin:
# Direction GPIO pin (high indicates positive direction). This
# parameter must be provided.
enable_pin:
# Enable pin (default is enable high; use ! to indicate enable
# low). If this parameter is not provided then the stepper motor
# driver must always be enabled.
rotation_distance:
# Distance (in mm) that the axis travels with one full rotation of
# the stepper motor. This parameter must be provided.
microsteps:
# The number of microsteps the stepper motor driver uses. This
# parameter must be provided.
full_steps_per_rotation: 200
# The number of full steps for one rotation of the stepper motor.
# Set this to 200 for a 1.8 degree stepper motor or set to 400 for a
# 0.9 degree motor. The default is 200.
gear_ratio:
# The gear ratio if the stepper motor is connected to the axis via a
# gearbox. For example, one may specify "5:1" if a 5 to 1 gearbox is
# in use. If the axis has multiple gearboxes one may specify a comma
# separated list of gear ratios (for example, "57:11, 2:1"). If a
# gear_ratio is specified then rotation_distance specifies the
# distance the axis travels for one full rotation of the final gear.
# The default is to not use a gear ratio.
endstop_pin:
# Endstop switch detection pin. This parameter must be provided for
# the X, Y, and Z steppers on cartesian style printers.
position_min: 0
# Minimum valid distance (in mm) the user may command the stepper to
# move to. The default is 0mm.
position_endstop:
# Location of the endstop (in mm). This parameter must be provided
# for the X, Y, and Z steppers on cartesian style printers.
position_max:
# Maximum valid distance (in mm) the user may command the stepper to
# move to. This parameter must be provided for the X, Y, and Z
# steppers on cartesian style printers.
homing_speed: 5.0
# Maximum velocity (in mm/s) of the stepper when homing. The default
# is 5mm/s.
homing_retract_dist: 5.0
# Distance to backoff (in mm) before homing a second time during
# homing. Set this to zero to disable the second home. The default
# is 5mm.
homing_retract_speed:
# Speed to use on the retract move after homing in case this should
# be different from the homing speed, which is the default for this
# parameter
second_homing_speed:
# Velocity (in mm/s) of the stepper when performing the second home.
# The default is homing_speed/2.
homing_positive_dir:
# If true, homing will cause the stepper to move in a positive
# direction (away from zero); if false, home towards zero. It is
# better to use the default than to specify this parameter. The
# default is true if position_endstop is near position_max and false
# if near position_min.
[tmc2209 stepper_x]
uart_pin: PB15
run_current: 0.580
hold_current: 0.500
stealthchop_threshold: 250
[stepper_y]
step_pin:
dir_pin:
enable_pin:
rotation_distance:
microsteps:
full_steps_per_rotation: 200
gear_ratio:
endstop_pin:
position_min: 0
position_endstop:
position_max:
homing_speed: 5.0
homing_retract_dist: 5.0
homing_retract_speed:
second_homing_speed:
homing_positive_dir:
[tmc2209 stepper_y]
uart_pin: PC6
run_current: 0.580
hold_current: 0.500
stealthchop_threshold: 250
[stepper_z]
step_pin:
dir_pin:
enable_pin:
rotation_distance:
microsteps:
full_steps_per_rotation: 200
gear_ratio:
endstop_pin:
position_min: 0
position_endstop:
position_max:
homing_speed: 5.0
homing_retract_dist: 5.0
homing_retract_speed:
second_homing_speed:
homing_positive_dir:
[tmc2209 stepper_z]
uart_pin: PC10
run_current: 0.580
hold_current: 0.500
stealthchop_threshold: 10
[safe_z_home]
home_xy_position: 70,100
speed: 50
z_hop: 10
z_hop_speed: 4
# --------------------For Delta Kinematics: --------------------------
# The stepper_a section describes the stepper controlling the front
# left tower (at 210 degrees). This section also controls the homing
# parameters (homing_speed, homing_retract_dist) for all towers.
[stepper_a]
position_endstop:
# Distance (in mm) between the nozzle and the bed when the nozzle is
# in the center of the build area and the endstop triggers. This
# parameter must be provided for stepper_a; for stepper_b and
# stepper_c this parameter defaults to the value specified for
# stepper_a.
arm_length:
# Length (in mm) of the diagonal rod that connects this tower to the
# print head. This parameter must be provided for stepper_a; for
# stepper_b and stepper_c this parameter defaults to the value
# specified for stepper_a.
#angle:
# This option specifies the angle (in degrees) that the tower is
# at. The default is 210 for stepper_a, 330 for stepper_b, and 90
# for stepper_c.
# The stepper_b section describes the stepper controlling the front
# right tower (at 330 degrees).
[stepper_b]
position_endstop:
arm_length:
# The stepper_c section describes the stepper controlling the rear
# tower (at 90 degrees).
[stepper_c]
position_endstop:
arm_length:
# The delta_calibrate section enables a DELTA_CALIBRATE extended
# g-code command that can calibrate the tower endstop positions and
# angles.
[delta_calibrate]
radius:
# Radius (in mm) of the area that may be probed. This is the radius
# of nozzle coordinates to be probed; if using an automatic probe
# with an XY offset then choose a radius small enough so that the
# probe always fits over the bed. This parameter must be provided.
speed: 50
# The speed (in mm/s) of non-probing moves during the calibration.
# The default is 50.
horizontal_move_z: 5
# The height (in mm) that the head should be commanded to move to
# just prior to starting a probe operation. The default is 5.
# -------------- For Polar Kinematics ---------------------------
# The stepper_bed section is used to describe the stepper controlling
# the bed.
[stepper_bed]
gear_ratio:
# A gear_ratio must be specified and rotation_distance may not be
# specified. For example, if the bed has an 80 toothed pulley driven
# by a stepper with a 16 toothed pulley then one would specify a
# gear ratio of "80:16". This parameter must be provided.
max_z_velocity:
# This sets the maximum velocity (in mm/s) of movement along the z
# axis. This setting can be used to restrict the maximum speed of
# the z stepper motor. The default is to use max_velocity for
# max_z_velocity.
max_z_accel:
# This sets the maximum acceleration (in mm/s^2) of movement along
# the z axis. It limits the acceleration of the z stepper motor. The
# default is to use max_accel for max_z_accel.
# The stepper_arm section is used to describe the stepper controlling
# the carriage on the arm.
[stepper_arm]
# --------------------- For
[extruder]
step_pin: PB3
dir_pin: !PB4
enable_pin: !PD2
microsteps: 16
rotation_distance: 33.400
nozzle_diameter: 0.400
filament_diameter: 1.750
heater_pin: PC8
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PA0
control: pid
pid_Kp=26.049
pid_Ki=1.258
pid_Kd=134.805
min_temp: 0
max_temp: 250
pressure_advance: 0.07
pressure_advance_smooth_time: 0.040
[tmc2209 extruder]
uart_pin: PC11
run_current: 0.670
hold_current: 0.500
stealthchop_threshold: 5
[heater_bed]
heater_pin: PC9
sensor_type: ATC Semitec 104GT-2
sensor_pin: PC3
control: pid
pid_Kp=72.740
pid_Ki=1.569
pid_Kd=842.877
min_temp: 0
max_temp: 130
[fan]
pin: PA8
[mcu]
serial:
# The serial port to connect to the MCU. If unsure (or if it
# changes) see the "Where's my serial port?" section of the FAQ.
# This parameter must be provided when using a serial port.
baud: 250000
# The baud rate to use. The default is 250000.
canbus_uuid:
# If using a device connected to a CAN bus then this sets the unique
# chip identifier to connect to. This value must be provided when using
# CAN bus for communication.
canbus_interface:
# If using a device connected to a CAN bus then this sets the CAN
# network interface to use. The default is 'can0'.
pin_map:
# This option may be used to enable Arduino pin name aliases. The
# default is to not enable the aliases.
restart_method:
# This controls the mechanism the host will use to reset the
# micro-controller. The choices are 'arduino', 'cheetah', 'rpi_usb',
# and 'command'. The 'arduino' method (toggle DTR) is common on
# Arduino boards and clones. The 'cheetah' method is a special
# method needed for some Fysetc Cheetah boards. The 'rpi_usb' method
# is useful on Raspberry Pi boards with micro-controllers powered
# over USB - it briefly disables power to all USB ports to
# accomplish a micro-controller reset. The 'command' method involves
# sending a Klipper command to the micro-controller so that it can
# reset itself. The default is 'arduino' if the micro-controller
# communicates over a serial port, 'command' otherwise.
[mcu my_extra_mcu]
serial:
baud: 250000
canbus_uuid:
canbus_interface:
pin_map:
restart_method:
[printer]
kinematics:
# The type of printer in use. This option may be one of: cartesian,
# corexy, corexz, delta, rotary_delta, polar, winch, or none. This
# parameter must be specified.
max_velocity:
# Maximum velocity (in mm/s) of the toolhead (relative to the
# print). This parameter must be specified.
max_accel:
# Maximum acceleration (in mm/s^2) of the toolhead (relative to the
# print). This parameter must be specified.
max_accel_to_decel:
# A pseudo acceleration (in mm/s^2) controlling how fast the
# toolhead may go from acceleration to deceleration. It is used to
# reduce the top speed of short zig-zag moves (and thus reduce
# printer vibration from these moves). The default is half of
# max_accel.
square_corner_velocity: 5.0
# The maximum velocity (in mm/s) that the toolhead may travel a 90
# degree corner at. A non-zero value can reduce changes in extruder
# flow rates by enabling instantaneous velocity changes of the
# toolhead during cornering. This value configures the internal
# centripetal velocity cornering algorithm; corners with angles
# larger than 90 degrees will have a higher cornering velocity while
# corners with angles less than 90 degrees will have a lower
# cornering velocity. If this is set to zero then the toolhead will
# decelerate to zero at each corner. The default is 5mm/s.
#
# -----delta Kinematics:------
max_z_velocity:
# For delta printers this limits the maximum velocity (in mm/s) of
# moves with z axis movement. This setting can be used to reduce the
# maximum speed of up/down moves (which require a higher step rate
# than other moves on a delta printer). The default is to use
# max_velocity for max_z_velocity.
minimum_z_position: 0
# The minimum Z position that the user may command the head to move
# to. The default is 0.
delta_radius:
# Radius (in mm) of the horizontal circle formed by the three linear
# axis towers. This parameter may also be calculated as:
# delta_radius = smooth_rod_offset - effector_offset - carriage_offset
# This parameter must be provided.
print_radius:
# The radius (in mm) of valid toolhead XY coordinates. One may use
# this setting to customize the range checking of toolhead moves. If
# a large value is specified here then it may be possible to command
# the toolhead into a collision with a tower. The default is to use
# delta_radius for print_radius (which would normally prevent a
# tower collision).
max_z_velocity: 5
max_z_accel: 100
max_accel_to_decel: 1000
square_corner_velocity: 5.0
[bed_screws]
screw1: 30,30
screw2: 30,200
screw3: 200,30
screw4: 200,200
[bed_mesh]
speed: 50
horizontal_move_z: 5
mesh_min: 48,8
mesh_max: 200,230
probe_count: 7,7
fade_start: 1.0
fade_end: 0.0
umber of
algorithm: bicubic
[static_digital_output usb_pullup_enable]
pins: !PC13
[gcode_arcs]
resolution: 1.0
[bltouch]
sensor_pin: ^PC14
control_pin: PA1
x_offset: 48
y_offset: -2
z_offset: 1.23
pin_move_time: 0.75
speed: 1.0
lift_speed: 3.0
samples: 2
sample_retract_dist: 3
[board_pins]
aliases:
# EXP1 header
EXP1_1=PB5, EXP1_3=PA9, EXP1_5=PA10, EXP1_7=PB8, EXP1_9=<GND>,
EXP1_2=PB6, EXP1_4=<RST>, EXP1_6=PB9, EXP1_8=PB7, EXP1_10=<5V>
[display]
lcd_type: st7920
cs_pin: EXP1_7
sclk_pin: EXP1_6
sid_pin: EXP1_8
encoder_pins: ^EXP1_5, ^EXP1_3
click_pin: ^!EXP1_2
[output_pin beeper]
pin: EXP1_1
[firmware_retraction]
retract_length: 2.7
retract_speed: 25
unretract_extra_length: 0.0
unretract_speed: 25
[virtual_sdcard]
path: /home/pi/.octoprint/uploads
[gcode_macro DO_MESH]
gcode:
M140 S60
G28
M190 S60 ; Heat Bed to 60C
BED_MESH_CALIBRATE ;Mesh leveling
M140 S0 ;Turn-off bed
SAVE_CONFIG
[gcode_macro CHECK_DISTANCE]
gcode:
M140 S60
G28
M190 S60 ; Heat Bed to 60C
G1 Z15.8 F1000
[gcode_macro START_PRINT]
gcode:
M220 S100 ;Reset Feedrate
M221 S100 ;Reset Flowrate
G92 E0 ;Reset Extruder
G1 Z1.0 F3000 ;Move Z Axis up
G1 X0 Y20 Z0.3 F5000.0 ;Move to start position
G1 X0 Y200.0 Z0.3 F1500.0 E15 ;Draw the first line
G1 X0.4 Y200.0 Z0.3 F5000.0 ;Move to side a little
G1 X0.4 Y20 Z0.3 F1500.0 E27 ;Draw the second line
G1 Z1.6 F3000 ;Move Z Axis up
[gcode_macro END_PRINT]
gcode:
;MESH:ENDGCODE
G91 ;Relative positioning
G92 E0 ;Reset Extruder
G1 F2400 E-8
G1 Z0.2 F2400 ;Raise Z
G1 X5 Y5 F3000 ;Wipe out
G1 Z10 ;Raise Z more
G90 ;Absolute positionning
G1 X0 Y{machine_depth} ;Present print
M106 S0 ;Turn-off fan
M140 S0 ;Turn-off bed
M84 X Y E ;Disable all steppers but Z
[gcode_macro SET_RETRACTIONLENGTH]
gcode:
SET_RETRACTION RETRACT_LENGTH={params.LENGTH|float}
GET_RETRACTION
[pause_resume]
# M600: Filament Change. This macro will pause the printer, move the
# tool to the change position, and retract the filament 50mm. Adjust
# the retraction settings for your own extruder. After filament has
# been changed, the print can be resumed from its previous position
# with the "RESUME" gcode.
[gcode_macro PARK]
gcode:
G1 X125 Y200.0 Z200.0 F4000
[gcode_macro FILAMENT_LOAD]
gcode:
M83 ; set e to relative positioning
G92 E0.0
G1 E70 F500 ; Initially go fast
G92 E0.0
G1 E10 F200 ; then go slow
G92 E0.0
[gcode_macro FILAMENT_UNLOAD]
gcode:
M83 ; set e to relative positioning
# wiggle filament out of the nozzle
G1 E0.5 F1000
G1 E-0.5 F1000
G1 E1.0 F1000
G1 E-1.0 F1000
G1 E1.5 F1000
G1 E-1.5 F1000
G1 E2.0 F1000
G1 E-100.0 F3000 ;fully unload
G92 E0.0
[gcode_macro M600]
default_parameter_X: 0
default_parameter_Y: 0
default_parameter_Z: 50
gcode:
SAVE_GCODE_STATE NAME=M600_state
PAUSE
G91 ;relative
G1 E-.2 F2700
G1 Z{Z} ;raise nozzle
G90 ;absolute
G1 X{X} Y{Y} F3000
G91 ;relative
M300 P1000 ;beep
FILAMENT_UNLOAD ;
M300 P3000 ;beep
G90 ;absolute
RESTORE_GCODE_STATE NAME=M600_state
#*# <---------------------- SAVE_CONFIG ---------------------->
#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
#*#
#*# [bed_mesh default]
#*# version = 1
#*# points =
#*# 0.087500, 0.080000, 0.068750, 0.047500, 0.075000, 0.078750, 0.085000
#*# 0.068750, 0.065000, 0.055000, 0.028750, 0.052500, 0.055000, 0.057500
#*# 0.092500, 0.091250, 0.082500, 0.048750, 0.070000, 0.063750, 0.066250
#*# 0.116250, 0.132500, 0.122500, 0.090000, 0.105000, 0.100000, 0.092500
#*# 0.128750, 0.135000, 0.130000, 0.086250, 0.096250, 0.076250, 0.076250
#*# 0.112500, 0.126250, 0.128750, 0.107500, 0.122500, 0.127500, 0.125000
#*# 0.120000, 0.133750, 0.141250, 0.097500, 0.125000, 0.106250, 0.100000
#*# tension = 0.2
#*# min_x = 48.0
#*# algo = bicubic
#*# y_count = 7
#*# mesh_y_pps = 2
#*# min_y = 8.0
#*# x_count = 7
#*# max_y = 230.0
#*# mesh_x_pps = 2
#*# max_x = 199.97

View File

@ -4,12 +4,12 @@
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
@ -27,12 +27,24 @@ class KlipperLogAnalyzer():
def __init__(self, log_file):
self.log_file = log_file
def read_log_file(self, logname):
try:
f = open(logname, 'r')
logdata = f.read()
f.close()
except IOError:
print("Couldn't open log file")
return logdata
def analyze(self):
data = self.parse_log(self.log_file, None)
if not data:
result = dict(error= "No relevant data available in \"{}\"".format(self.log_file))
result1 = dict(error= "No relevant data available in \"{}\"".format(self.log_file))
else:
result = self.plot_mcu(data, self.MAXBANDWIDTH)
result1 = self.plot_mcu(data, self.MAXBANDWIDTH)
result = dict(plot = result1,
logfiledata = self.read_log_file(self.log_file)
)
return result
def parse_log(self, logname, mcu):

View File

@ -112,10 +112,23 @@ ul#klipper-settings {
padding-top: 2px;
}
#klipper_graph_dialog {
width: 1050px;
}
#klipper_graph_dialog .full-sized-box{
width: 1000px;
margin: 0 auto;
}
#klipper_graph_dialog form {
margin: 0;
}
#klipper_graph_dialog select {
width: auto;
}
#klipper_graph_dialog .graph-footer {
bottom:0;
}
@ -133,8 +146,14 @@ ul#klipper-settings {
#klipper_graph_dialog .fill-checkbox {
display: block;
position: absolute;
bottom: 5px;
margin-left: 10px;
top: 0%;
left: 50%;
}
#klipper_graph_dialog .help-inline {
display: block;
position: absolute;
top: 0px;
}
#klipper_graph_canvas {

View File

@ -16,6 +16,14 @@
$(function () {
function KlipperViewModel(parameters) {
var self = this;
var console_debug = false;
self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json",
"cache-control": "no-cache"
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.settings = parameters[0];
self.loginState = parameters[1];
@ -23,20 +31,24 @@ $(function () {
self.levelingViewModel = parameters[3];
self.paramMacroViewModel = parameters[4];
self.access = parameters[5];
self.shortStatus = ko.observable();
self.logMessages = ko.observableArray();
self.showPopUp = function(popupType, popupTitle, message){
var title = popupType.toUpperCase() + ": " + popupTitle;
new PNotify({
title: title,
text: message,
type: popupType,
hide: false
var title = popupType.toUpperCase() + ": " + popupTitle;
new PNotify({
title: title,
text: message,
type: popupType,
hide: false
});
};
self.onSettingsShown = function () {
self.reloadConfig();
}
self.showLevelingDialog = function () {
var dialog = $("#klipper_leveling_dialog");
dialog.modal({
@ -118,20 +130,31 @@ $(function () {
};
self.onDataUpdaterPluginMessage = function(plugin, data) {
if(plugin == "klipper") {
if ("warningPopUp" == data.type){
self.showPopUp(data.subtype, data.title, data.payload);
return;
}
if ("errorPopUp" == data.type){
self.showPopUp(data.subtype, data.title, data.payload);
return;
}
if(data.type == "status") {
self.shortStatus(data.payload);
} else {
self.logMessage(data.time, data.subtype, data.payload);
}
if(plugin == "klipper") {
if ("warningPopUp" == data.type){
self.showPopUp(data.subtype, data.title, data.payload);
return;
}
if ("errorPopUp" == data.type){
self.showPopUp(data.subtype, data.title, data.payload);
return;
}
if ("console" == data.type) {
self.consoleMessage(data.subtype, data.payload);
return;
}
if ("reload" == data.type){
return;
}
if(data.type == "status") {
self.shortStatus(data.payload);
} else {
self.logMessage(data.time, data.subtype, data.payload);
}
}
};
@ -143,12 +166,43 @@ $(function () {
});
};
self.reloadConfig = function() {
var settings = {
"crossDomain": true,
"url": self.apiUrl,
"method": "POST",
"headers": self.header,
"processData": false,
"dataType": "json",
"data": JSON.stringify({command: "reloadConfig"})
}
$.ajax(settings).done(function (response) {
self.consoleMessage(
"debug",
"Reloaded from Backend " + response);
});
}
self.consoleMessage = function (type, message) {
if (type == "info"){
console.info("OctoKlipper : " + message);
} else {
if (console_debug){
console.debug("OctoKlipper : " + message);
} else {
return
}
}
return
}
self.onClearLog = function () {
self.logMessages.removeAll();
};
self.isActive = function () {
return self.connectionState.isOperational() && this.hasRight("CONFIG");
return self.connectionState.isOperational();
};
self.hasRight = function (right_role, type) {
@ -167,8 +221,7 @@ $(function () {
$("a#navbar_show_settings").click();
$("li#settings_plugin_klipper_link a").click();
if (profile_type) {
var query =
"#klipper-settings a[data-profile-type='" + profile_type + "']";
var query = "#klipper-settings a[data-profile-type='" + profile_type + "']";
$(query).click();
}
};

View File

@ -4,12 +4,12 @@
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
@ -18,16 +18,17 @@ $(function() {
function KlipperGraphViewModel(parameters) {
var self = this;
self.loginState = parameters[0];
self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json",
"cache-control": "no-cache"
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.availableLogFiles = ko.observableArray();
self.logFile = ko.observable();
self.klippylogFile = ko.observable();
self.status = ko.observable();
self.datasets = ko.observableArray();
self.datasetFill = ko.observable(false);
@ -40,24 +41,24 @@ function KlipperGraphViewModel(parameters) {
self.canvas = $("#klipper_graph_canvas")[0]
self.canvasContext = self.canvas.getContext("2d");
self.spinnerDialog = $("#klipper_graph_spinner");
Chart.defaults.global.elements.line.borderWidth=1;
Chart.defaults.global.elements.line.fill= false;
Chart.defaults.global.elements.point.radius= 0;
var myChart = new Chart(self.canvas, {
type: "line"
});
if(self.loginState.loggedIn()) {
self.listLogFiles();
}
}
self.onUserLoggedIn = function(user) {
self.listLogFiles();
}
self.listLogFiles = function() {
var settings = {
"crossDomain": true,
@ -68,31 +69,31 @@ function KlipperGraphViewModel(parameters) {
"dataType": "json",
"data": JSON.stringify({command: "listLogFiles"})
}
$.ajax(settings).done(function (response) {
self.availableLogFiles.removeAll();
self.availableLogFiles(response["data"]);
});
}
self.saveGraphToPng = function() {
button = $('#download-btn');
var dataURL = self.canvas.toDataURL("image/png");//.replace("image/png", "image/octet-stream");
button.attr("href", dataURL);
}
self.showSpinner = function(showDialog) {
if (showDialog) {
self.spinnerDialog.modal({
show: true,
keyboard: false,
backdrop: "static"
backdrop: "static"
});
} else {
self.spinnerDialog.modal("hide");
}
}
self.toggleDatasetFill = function() {
if(self.datasets) {
for (i=0; i < self.datasets().length; i++) {
@ -102,11 +103,11 @@ function KlipperGraphViewModel(parameters) {
}
return true
}
self.convertTime = function(val) {
return moment(val, "X");
}
self.loadData = function() {
var settings = {
"crossDomain": true,
@ -122,17 +123,18 @@ function KlipperGraphViewModel(parameters) {
}
)
}
self.showSpinner(true);
$.ajax(settings).done(function (response) {
self.status("")
self.datasetFill(false);
self.showSpinner(false);
if("error" in response) {
self.status(response.error);
self.klippylogFile(response.logfiledata);
if("error" in response.plot) {
self.status(response.plot.error);
} else {
self.datasets.removeAll();
self.datasets.push(
@ -141,40 +143,40 @@ function KlipperGraphViewModel(parameters) {
backgroundColor: "rgba(199, 44, 59, 0.5)",
borderColor: "rgb(199, 44, 59)",
yAxisID: 'y-axis-1',
data: response.loads
data: response.plot.loads
});
self.datasets.push(
{
label: "Bandwith",
backgroundColor: "rgba(255, 130, 1, 0.5)",
borderColor: "rgb(255, 130, 1)",
yAxisID: 'y-axis-1',
data: response.bwdeltas
data: response.plot.bwdeltas
});
self.datasets.push(
{
label: "Host Buffer",
backgroundColor: "rgba(0, 145, 106, 0.5)",
borderColor: "rgb(0, 145, 106)",
yAxisID: 'y-axis-1',
data: response.buffers
data: response.plot.buffers
});
self.datasets.push(
{
label: "Awake Time",
backgroundColor: "rgba(33, 64, 95, 0.5)",
borderColor: "rgb(33, 64, 95)",
yAxisID: 'y-axis-1',
data: response.awake
data: response.plot.awake
});
self.chart = new Chart(self.canvas, {
type: "line",
data: {
labels: response.times,
labels: response.plot.times,
datasets: self.datasets()
},
options: {
@ -211,7 +213,7 @@ function KlipperGraphViewModel(parameters) {
]
},
legend: {
}
}
});

View File

@ -4,12 +4,12 @@
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
@ -17,64 +17,130 @@ $(function() {
$('#klipper-settings a:first').tab('show');
function KlipperSettingsViewModel(parameters) {
var self = this;
var obKlipperConfig = null;
var editor = null;
self.settings = parameters[0];
self.header = OctoPrint.getRequestHeaders({
"content-type": "application/json",
"cache-control": "no-cache"
});
self.apiUrl = OctoPrint.getSimpleApiUrl("klipper");
self.addMacro = function() {
self.settings.settings.plugins.klipper.macros.push({
name: 'Macro',
macro: '',
sidebar: true,
tab: true
});
self.settings.settings.plugins.klipper.macros.push({
name: 'Macro',
macro: '',
sidebar: true,
tab: true
});
}
self.removeMacro = function(macro) {
self.settings.settings.plugins.klipper.macros.remove(macro);
self.settings.settings.plugins.klipper.macros.remove(macro);
}
self.moveMacroUp = function(macro) {
self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro)
self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro)
}
self.moveMacroDown = function(macro) {
self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro)
self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro)
}
self.addProbePoint = function() {
self.settings.settings.plugins.klipper.probe.points.push(
{
name: 'point-#',
x:0, y:0, z:0
}
);
self.settings.settings.plugins.klipper.probe.points.push(
{
name: 'point-#',
x:0, y:0, z:0
}
);
}
self.removeProbePoint = function(point) {
self.settings.settings.plugins.klipper.probe.points.remove(point);
self.settings.settings.plugins.klipper.probe.points.remove(point);
}
self.moveProbePointUp = function(macro) {
self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro)
self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro)
}
self.moveProbePointDown = function(macro) {
self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro)
self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro)
}
self.moveItemDown = function(list, item) {
var i = list().indexOf(item);
if (i < list().length - 1) {
var rawList = list();
list.splice(i, 2, rawList[i + 1], rawList[i]);
}
var i = list().indexOf(item);
if (i < list().length - 1) {
var rawList = list();
list.splice(i, 2, rawList[i + 1], rawList[i]);
}
}
self.moveItemUp = function(list, item) {
var i = list().indexOf(item);
if (i > 0) {
var rawList = list();
list.splice(i-1, 2, rawList[i], rawList[i-1]);
}
var i = list().indexOf(item);
if (i > 0) {
var rawList = list();
list.splice(i-1, 2, rawList[i], rawList[i-1]);
}
}
self.configBound = function (config) {
config.withSilence = function() {
this.notifySubscribers = function() {
if (!this.isSilent) {
ko.subscribable.fn.notifySubscribers.apply(this, arguments);
}
}
this.silentUpdate = function(newValue) {
this.isSilent = true;
this(newValue);
this.isSilent = false;
};
return this;
}
obKlipperConfig = config.withSilence();
if (editor) {
editor.setValue(obKlipperConfig());
editor.clearSelection();
}
return obKlipperConfig;
}
ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/");
editor = ace.edit("plugin-klipper-config");
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/klipper_config");
editor.setOptions({
autoScrollEditorIntoView: true,
maxLines: "Infinity"
})
editor.session.on('change', function(delta) {
if (obKlipperConfig) {
obKlipperConfig.silentUpdate(editor.getValue());
}
});
// Uncomment this if not using maxLines: "Infinity"...
// setInterval(function(){ editor.resize(); }, 500);
self.onDataUpdaterPluginMessage = function(plugin, data) {
if(plugin == "klipper") {
if ("reload" == data.type){
if ("config" == data.subtype){
if (editor.session) {
editor.session.setValue(data.payload);
editor.clearSelection();
}
}
return
}
}
}
}

View File

@ -5,12 +5,12 @@
</div>
<div class="modal-body">
<div class="full-sized-box">
<span class="help-inline" style="display:block; position: absolute">
<em>Click labels to hide/show dataset</em>
</span>
<script src="plugin/klipper/static/js/lib/Chart.bundle.min.js" type="text/javascript" defer></script>
<canvas id="klipper_graph_canvas"></canvas>
</div>
<span class="help-inline" style="display:block; position: absolute">
<em>Click labels to hide/show dataset</em>
</span>
<label class="checkbox fill-checkbox">
<input type="checkbox" data-bind="checked: datasetFill, click: toggleDatasetFill" />{{ _('Fill Datasets') }}
</label>
@ -26,6 +26,7 @@
<button class="btn" data-bind="click: loadData"><i class="icon-signal"> </i>{{ _('Analyze Log') }}</button>
<button class="btn" data-dismiss="modal"><i class="icon-remove"> </i>{{ _('Close') }}</button>
</form>
<textarea readonly id="plugin-klipper-klippylog" rows="31" class="block" data-bind="value: klippylogFile"></textarea>
</div>
</div>
<div id="klipper_graph_spinner" class="modal hide fade small" tabindex="-1" role="dialog" aria-hidden="true">

View File

@ -26,13 +26,13 @@
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Klipper Config Path') }}</label>
<label class="control-label">{{ _('Klipper Config File') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.configpath">
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Klipper Log Path') }}</label>
<label class="control-label">{{ _('Klipper Log File') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: settings.settings.plugins.klipper.configuration.logpath">
</div>
@ -61,7 +61,7 @@
</div>
</div>
</div>
<div data-bind="foreach: settings.settings.plugins.klipper.macros">
<div data-bind="foreach: settings.settings.plugins.klipper.macros">
<div class="control-group" id="item">
<label class="control-label">{{ _('Name') }}</label>
<div class="controls">
@ -197,57 +197,11 @@
<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>
<!--
<a href='#' data-bind='click: onReloadConfig'><i class="icon-refresh"></i></a> {{ _('Reload Configfile') }}
-->
<input id="hdnLoadKlipperConfig" type="hidden" data-bind="value: configBound(settings.settings.plugins.klipper.config)" />
<div id="plugin-klipper-config"></div>
<script>
var obKlipperConfig = null;
var editor = null;
function configBound(config) {
config.withSilence = function() {
this.notifySubscribers = function() {
if (!this.isSilent) {
ko.subscribable.fn.notifySubscribers.apply(this, arguments);
}
};
this.silentUpdate = function(newValue) {
this.isSilent = true;
this(newValue);
this.isSilent = false;
};
return this;
};
obKlipperConfig = config.withSilence();
if (editor) {
editor.setValue(obKlipperConfig());
editor.clearSelection();
}
return obKlipperConfig;
}
ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/");
editor = ace.edit("plugin-klipper-config");
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/klipper_config");
editor.setOptions({
autoScrollEditorIntoView: true,
maxLines: "Infinity"
});
editor.session.on('change', function(delta) {
if (obKlipperConfig) {
obKlipperConfig.silentUpdate(editor.getValue());
}
});
// Uncomment this if not using maxLines: "Infinity"...
// setInterval(function(){ editor.resize(); }, 500);
</script>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
</div>
</div>
&nbsp;
<button class="btn btn-mini pull-right clear-btn" data-bind="click: onClearLog, enable: isActive()"
<button class="btn btn-mini pull-right clear-btn" data-bind="click: onClearLog"
title="Clear Log">
<i class="fa fa-trash"></i> {{ _('Clear') }}
</button>
@ -56,9 +56,9 @@
title="Sets a offset for subsequent GCODE coordinates.">
{{ _('Coordinate Offset') }}
</button>
<button class="btn btn-block btn-small" data-bind="click: showGraphDialog, enable: isActive()"
<button class="btn btn-block btn-small" data-bind="click: showGraphDialog"
title="Assists in debugging performance issues by analyzing the Klipper log files.">
{{ _('Performance Graph') }}
{{ _('Analyze Klipper Logfiles') }}
</button>
</div>
</div>
@ -73,4 +73,4 @@
</div>
</div>
</div>
</div>
</div>