[feat] Add performance graph (#9)
* [feat] Basic graph demo * [feat] Basic Log analyzer with graph * [feat] Finish graph feature.
This commit is contained in:
parent
4856ae32c1
commit
86d8e631d5
|
@ -4,7 +4,10 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
import octoprint.plugin
|
import octoprint.plugin
|
||||||
import octoprint.plugin.core
|
import octoprint.plugin.core
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
from octoprint.util.comm import parse_firmware_line
|
from octoprint.util.comm import parse_firmware_line
|
||||||
|
from .modules import KlipperLogAnalyzer
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
class KlipperPlugin(
|
class KlipperPlugin(
|
||||||
|
@ -12,6 +15,7 @@ class KlipperPlugin(
|
||||||
octoprint.plugin.TemplatePlugin,
|
octoprint.plugin.TemplatePlugin,
|
||||||
octoprint.plugin.SettingsPlugin,
|
octoprint.plugin.SettingsPlugin,
|
||||||
octoprint.plugin.AssetPlugin,
|
octoprint.plugin.AssetPlugin,
|
||||||
|
octoprint.plugin.SimpleApiPlugin,
|
||||||
octoprint.plugin.EventHandlerPlugin):
|
octoprint.plugin.EventHandlerPlugin):
|
||||||
|
|
||||||
_parsing_response = False
|
_parsing_response = False
|
||||||
|
@ -192,6 +196,12 @@ class KlipperPlugin(
|
||||||
custom_bindings=True,
|
custom_bindings=True,
|
||||||
icon="rocket",
|
icon="rocket",
|
||||||
replaces= "connection" if self._settings.get_boolean(["connection", "replace_connection_panel"]) else ""
|
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
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -200,10 +210,12 @@ class KlipperPlugin(
|
||||||
def get_assets(self):
|
def get_assets(self):
|
||||||
return dict(
|
return dict(
|
||||||
js=["js/klipper.js",
|
js=["js/klipper.js",
|
||||||
"js/klipper_settings.js",
|
"js/klipper_settings.js",
|
||||||
"js/klipper_leveling.js",
|
"js/klipper_leveling.js",
|
||||||
"js/klipper_pid_tuning.js",
|
"js/klipper_pid_tuning.js",
|
||||||
"js/klipper_offset.js"],
|
"js/klipper_offset.js",
|
||||||
|
"js/klipper_graph.js"
|
||||||
|
],
|
||||||
css=["css/klipper.css"],
|
css=["css/klipper.css"],
|
||||||
less=["css/klipper.less"]
|
less=["css/klipper.less"]
|
||||||
)
|
)
|
||||||
|
@ -245,6 +257,33 @@ class KlipperPlugin(
|
||||||
self.logError(msg)
|
self.logError(msg)
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
def get_api_commands(self):
|
||||||
|
return dict(
|
||||||
|
listLogFiles=[],
|
||||||
|
getStats=["logFile"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_api_command(self, command, data):
|
||||||
|
if command == "listLogFiles":
|
||||||
|
files = []
|
||||||
|
for f in glob.glob("/tmp/*.log*"):
|
||||||
|
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())
|
||||||
|
|
||||||
|
def on_api_get(self, request):
|
||||||
|
log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer("/tmp/klippy.log.2018-08-06")
|
||||||
|
|
||||||
|
return log_analyzer.analyze()
|
||||||
|
|
||||||
def get_update_information(self):
|
def get_update_information(self):
|
||||||
return dict(
|
return dict(
|
||||||
klipper=dict(
|
klipper=dict(
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import optparse, datetime
|
||||||
|
|
||||||
|
class KlipperLogAnalyzer():
|
||||||
|
MAXBANDWIDTH=25000.
|
||||||
|
MAXBUFFER=2.
|
||||||
|
STATS_INTERVAL=5.
|
||||||
|
TASK_MAX=0.0025
|
||||||
|
APPLY_PREFIX = ['mcu_awake', 'mcu_task_avg', 'mcu_task_stddev', 'bytes_write',
|
||||||
|
'bytes_read', 'bytes_retransmit', 'freq', 'adj']
|
||||||
|
|
||||||
|
def __init__(self, log_file):
|
||||||
|
self.log_file = log_file
|
||||||
|
|
||||||
|
def analyze(self):
|
||||||
|
data = self.parse_log(self.log_file, None)
|
||||||
|
if not data:
|
||||||
|
result = dict(error= "Couldn't parse \"{}\"".format(self.log_file))
|
||||||
|
else:
|
||||||
|
result = self.plot_mcu(data, self.MAXBANDWIDTH)
|
||||||
|
#if options.frequency:
|
||||||
|
# plot_frequency(data, outname, options.mcu)
|
||||||
|
# return
|
||||||
|
return result
|
||||||
|
|
||||||
|
def parse_log(self, logname, mcu):
|
||||||
|
if mcu is None:
|
||||||
|
mcu = "mcu"
|
||||||
|
mcu_prefix = mcu + ":"
|
||||||
|
apply_prefix = { p: 1 for p in self.APPLY_PREFIX }
|
||||||
|
|
||||||
|
f = open(logname, 'rb')
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for line in f:
|
||||||
|
parts = line.split()
|
||||||
|
if not parts or parts[0] not in ('Stats', 'INFO:root:Stats'):
|
||||||
|
#if parts and parts[0] == 'INFO:root:shutdown:':
|
||||||
|
# break
|
||||||
|
continue
|
||||||
|
prefix = ""
|
||||||
|
keyparts = {}
|
||||||
|
for p in parts[2:]:
|
||||||
|
if '=' not in p:
|
||||||
|
prefix = p
|
||||||
|
if prefix == mcu_prefix:
|
||||||
|
prefix = ''
|
||||||
|
continue
|
||||||
|
name, val = p.split('=', 1)
|
||||||
|
if name in apply_prefix:
|
||||||
|
name = prefix + name
|
||||||
|
keyparts[name] = val
|
||||||
|
if keyparts.get('bytes_write', '0') == '0':
|
||||||
|
continue
|
||||||
|
keyparts['#sampletime'] = float(parts[1][:-1])
|
||||||
|
out.append(keyparts)
|
||||||
|
f.close()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def find_print_restarts(self, data):
|
||||||
|
runoff_samples = {}
|
||||||
|
last_runoff_start = last_buffer_time = last_sampletime = 0.
|
||||||
|
last_print_stall = 0
|
||||||
|
for d in reversed(data):
|
||||||
|
# Check for buffer runoff
|
||||||
|
sampletime = d['#sampletime']
|
||||||
|
buffer_time = float(d.get('buffer_time', 0.))
|
||||||
|
if (last_runoff_start and last_sampletime - sampletime < 5
|
||||||
|
and buffer_time > last_buffer_time):
|
||||||
|
runoff_samples[last_runoff_start][1].append(sampletime)
|
||||||
|
elif buffer_time < 1.:
|
||||||
|
last_runoff_start = sampletime
|
||||||
|
runoff_samples[last_runoff_start] = [False, [sampletime]]
|
||||||
|
else:
|
||||||
|
last_runoff_start = 0.
|
||||||
|
last_buffer_time = buffer_time
|
||||||
|
last_sampletime = sampletime
|
||||||
|
# Check for print stall
|
||||||
|
print_stall = int(d['print_stall'])
|
||||||
|
if print_stall < last_print_stall:
|
||||||
|
if last_runoff_start:
|
||||||
|
runoff_samples[last_runoff_start][0] = True
|
||||||
|
last_print_stall = print_stall
|
||||||
|
sample_resets = {sampletime: 1 for stall, samples in runoff_samples.values()
|
||||||
|
for sampletime in samples if not stall}
|
||||||
|
return sample_resets
|
||||||
|
|
||||||
|
def plot_mcu(self, data, maxbw):
|
||||||
|
# Generate data for plot
|
||||||
|
basetime = lasttime = data[0]['#sampletime']
|
||||||
|
lastbw = float(data[0]['bytes_write']) + float(data[0]['bytes_retransmit'])
|
||||||
|
sample_resets = self.find_print_restarts(data)
|
||||||
|
times = []
|
||||||
|
bwdeltas = []
|
||||||
|
loads = []
|
||||||
|
awake = []
|
||||||
|
hostbuffers = []
|
||||||
|
for d in data:
|
||||||
|
st = d['#sampletime']
|
||||||
|
timedelta = st - lasttime
|
||||||
|
if timedelta <= 0.:
|
||||||
|
continue
|
||||||
|
bw = float(d['bytes_write']) + float(d['bytes_retransmit'])
|
||||||
|
if bw < lastbw:
|
||||||
|
lastbw = bw
|
||||||
|
continue
|
||||||
|
load = float(d['mcu_task_avg']) + 3*float(d['mcu_task_stddev'])
|
||||||
|
if st - basetime < 15.:
|
||||||
|
load = 0.
|
||||||
|
pt = float(d['print_time'])
|
||||||
|
hb = float(d['buffer_time'])
|
||||||
|
if hb >= self.MAXBUFFER or st in sample_resets:
|
||||||
|
hb = 0.
|
||||||
|
else:
|
||||||
|
hb = 100. * (self.MAXBUFFER - hb) / self.MAXBUFFER
|
||||||
|
hostbuffers.append(hb)
|
||||||
|
#times.append(datetime.datetime.utcfromtimestamp(st))
|
||||||
|
times.append(st)
|
||||||
|
bwdeltas.append(100. * (bw - lastbw) / (maxbw * timedelta))
|
||||||
|
loads.append(100. * load / self.TASK_MAX)
|
||||||
|
awake.append(100. * float(d.get('mcu_awake', 0.)) / self.STATS_INTERVAL)
|
||||||
|
lasttime = st
|
||||||
|
lastbw = bw
|
||||||
|
|
||||||
|
result = dict(
|
||||||
|
times= times,
|
||||||
|
bwdeltas= bwdeltas,
|
||||||
|
loads= loads,
|
||||||
|
awake= awake,
|
||||||
|
buffers= hostbuffers
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def plot_frequency(self, data, mcu):
|
||||||
|
all_keys = {}
|
||||||
|
for d in data:
|
||||||
|
all_keys.update(d)
|
||||||
|
one_mcu = mcu is not None
|
||||||
|
graph_keys = { key: ([], []) for key in all_keys
|
||||||
|
if (key in ("freq", "adj") or (not one_mcu and (
|
||||||
|
key.endswith(":freq") or key.endswith(":adj")))) }
|
||||||
|
basetime = lasttime = data[0]['#sampletime']
|
||||||
|
for d in data:
|
||||||
|
st = d['#sampletime']
|
||||||
|
for key, (times, values) in graph_keys.items():
|
||||||
|
val = d.get(key)
|
||||||
|
if val not in (None, '0', '1'):
|
||||||
|
times.append(st)
|
||||||
|
values.append(float(val))
|
|
@ -38,4 +38,16 @@
|
||||||
|
|
||||||
#plugin-klipper-config {
|
#plugin-klipper-config {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#klipper_graph_dialog form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#klipper_graph_dialog .graph-footer {
|
||||||
|
bottom:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#klipper_graph_dialog input {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
|
@ -5,35 +5,43 @@ $(function() {
|
||||||
self.settings = parameters[0];
|
self.settings = parameters[0];
|
||||||
self.loginState = parameters[1];
|
self.loginState = parameters[1];
|
||||||
self.connectionState = parameters[2];
|
self.connectionState = parameters[2];
|
||||||
|
|
||||||
self.shortStatus = ko.observable();
|
self.shortStatus = ko.observable();
|
||||||
self.logMessages = ko.observableArray();
|
self.logMessages = ko.observableArray();
|
||||||
|
|
||||||
self.showLevelingDialog = function() {
|
self.showLevelingDialog = function() {
|
||||||
var dialog = $("#klipper_leveling_dialog");
|
var dialog = $("#klipper_leveling_dialog");
|
||||||
dialog.modal({
|
dialog.modal({
|
||||||
show: 'true',
|
show: 'true',
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
keyboard: false
|
keyboard: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.showPidTuningDialog = function() {
|
self.showPidTuningDialog = function() {
|
||||||
var dialog = $("#klipper_pid_tuning_dialog");
|
var dialog = $("#klipper_pid_tuning_dialog");
|
||||||
dialog.modal({
|
dialog.modal({
|
||||||
show: 'true',
|
show: 'true',
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
keyboard: false
|
keyboard: false
|
||||||
});;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.showOffsetDialog = function() {
|
self.showOffsetDialog = function() {
|
||||||
var dialog = $("#klipper_offset_dialog");
|
var dialog = $("#klipper_offset_dialog");
|
||||||
dialog.modal({
|
dialog.modal({
|
||||||
show: 'true',
|
show: 'true',
|
||||||
backdrop: 'static',
|
backdrop: 'static'
|
||||||
keyboard: false
|
});
|
||||||
});;
|
}
|
||||||
|
|
||||||
|
self.showGraphDialog = function() {
|
||||||
|
var dialog = $("#klipper_graph_dialog");
|
||||||
|
dialog.modal({
|
||||||
|
show: 'true',
|
||||||
|
minHeight: "500px",
|
||||||
|
maxHeight: "600px"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeMacro = function(macro) {
|
self.executeMacro = function(macro) {
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
function KlipperGraphViewModel(parameters) {
|
||||||
|
var self = this;
|
||||||
|
self.header = {
|
||||||
|
"x-api-key": "2CE2F6BA87244897B7F3A1BED3B1A3ED",
|
||||||
|
"content-type": "application/json",
|
||||||
|
"cache-control": "no-cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.apiUrl = "/api/plugin/klipper"
|
||||||
|
|
||||||
|
self.availableLogFiles = ko.observableArray();
|
||||||
|
self.logFile = ko.observable();
|
||||||
|
self.status = ko.observable();
|
||||||
|
self.datasets = ko.observableArray();
|
||||||
|
self.canvas;
|
||||||
|
self.canvasContext;
|
||||||
|
self.chart;
|
||||||
|
self.datasetFill = ko.observable(false);
|
||||||
|
|
||||||
|
self.onStartup = function() {
|
||||||
|
self.canvas = $("#klipper_graph_canvas")[0]
|
||||||
|
self.canvasContext = self.canvas.getContext("2d");
|
||||||
|
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
self.listLogFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.listLogFiles = function() {
|
||||||
|
var settings = {
|
||||||
|
"url": self.apiUrl,
|
||||||
|
"method": "POST",
|
||||||
|
"headers": self.header,
|
||||||
|
"processData": false,
|
||||||
|
"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.toggleDatasetFill = function() {
|
||||||
|
if(self.datasets) {
|
||||||
|
for (i=0; i < self.datasets().length; i++) {
|
||||||
|
self.datasets()[i].fill = self.datasetFill();
|
||||||
|
}
|
||||||
|
self.chart.update();
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.convertTime = function(val) {
|
||||||
|
return moment(val, "X");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadData = function() {
|
||||||
|
var settings = {
|
||||||
|
"crossDomain": true,
|
||||||
|
"url": self.apiUrl,
|
||||||
|
"method": "POST",
|
||||||
|
"headers": self.header,
|
||||||
|
"processData": false,
|
||||||
|
"dataType": "json",
|
||||||
|
"data": JSON.stringify(
|
||||||
|
{
|
||||||
|
command: "getStats",
|
||||||
|
logFile: self.logFile()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax(settings).done(function (response) {
|
||||||
|
self.status("")
|
||||||
|
self.datasetFill(false);
|
||||||
|
if("error" in response) {
|
||||||
|
self.status(response.error);
|
||||||
|
} else {
|
||||||
|
self.datasets.removeAll();
|
||||||
|
self.datasets.push(
|
||||||
|
{
|
||||||
|
label: "MCU Load",
|
||||||
|
backgroundColor: "rgba(199, 44, 59, 0.5)",
|
||||||
|
borderColor: "rgb(199, 44, 59)",
|
||||||
|
data: response.loads
|
||||||
|
});
|
||||||
|
|
||||||
|
self.datasets.push(
|
||||||
|
{
|
||||||
|
label: "Bandwith",
|
||||||
|
backgroundColor: "rgba(255, 130, 1, 0.5)",
|
||||||
|
borderColor: "rgb(255, 130, 1)",
|
||||||
|
data: response.bwdeltas
|
||||||
|
});
|
||||||
|
|
||||||
|
self.datasets.push(
|
||||||
|
{
|
||||||
|
label: "Host Buffer",
|
||||||
|
backgroundColor: "rgba(0, 145, 106, 0.5)",
|
||||||
|
borderColor: "rgb(0, 145, 106)",
|
||||||
|
data: response.buffers
|
||||||
|
});
|
||||||
|
|
||||||
|
self.datasets.push(
|
||||||
|
{
|
||||||
|
label: "Awake Time",
|
||||||
|
backgroundColor: "rgba(33, 64, 95, 0.5)",
|
||||||
|
borderColor: "rgb(33, 64, 95)",
|
||||||
|
data: response.awake
|
||||||
|
});
|
||||||
|
|
||||||
|
self.chart = new Chart(self.canvas, {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: response.times,
|
||||||
|
datasets: self.datasets()
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
elements:{
|
||||||
|
line: {
|
||||||
|
tension: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
parser: self.convertTime,
|
||||||
|
tooltipFormat: "HH:mm",
|
||||||
|
displayFormats: {
|
||||||
|
minute: "HH:mm",
|
||||||
|
second: "HH:mm",
|
||||||
|
millisecond: "HH:mm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Time'
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: '%'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OCTOPRINT_VIEWMODELS.push({
|
||||||
|
construct: KlipperGraphViewModel,
|
||||||
|
dependencies: [],
|
||||||
|
elements: ["#klipper_graph_dialog"]
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,27 @@
|
||||||
|
<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">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h3 id="klipper_pid_tuning_dialog_label">{{ _('Performance Graph') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="full-sized-box">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<form class="form-inline">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" data-bind="checked: datasetFill, click: toggleDatasetFill" />{{ _('filled datasets') }}
|
||||||
|
</label>
|
||||||
|
<span class="help-inline" text-align="left" data-bind="text: status"></span>
|
||||||
|
<label class="control-label">
|
||||||
|
{{ _('Select Log') }}
|
||||||
|
<select data-bind="options: availableLogFiles, optionsText: 'name', optionsValue: 'file', value: logFile"></select>
|
||||||
|
</label>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -12,29 +12,33 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls">
|
<div class="control-group">
|
||||||
|
<div class="controls">
|
||||||
<label class="control-label"></label>
|
<label class="control-label"></label>
|
||||||
<button class="btn btn-block" data-bind="click: onGetStatus, enable: isActive()"><i class="fa icon-black fa-info-circle"></i> {{ _('Get Status') }}</button>
|
<button class="btn btn-block btn-small" data-bind="click: onGetStatus, enable: isActive()"><i class="fa icon-black fa-info-circle"></i> {{ _('Get Status') }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="control-group">
|
||||||
<hr>
|
<div class="controls">
|
||||||
<label class="control-label"><i class="icon-refresh"></i> {{ _('Restart') }}</label>
|
<label class="control-label small"><i class="icon-refresh"></i> {{ _('Restart') }}</label>
|
||||||
<button class="btn btn-block" data-bind="click: onRestartHost, enable: isActive()">{{ _('Host') }}</button>
|
<button class="btn btn-block btn-small" data-bind="click: onRestartHost, enable: isActive()">{{ _('Host') }}</button>
|
||||||
<button class="btn btn-block" data-bind="click: onRestartFirmware, enable: isActive()">{{ _('Firmware') }}</button>
|
<button class="btn btn-block btn-small" data-bind="click: onRestartFirmware, enable: isActive()">{{ _('Firmware') }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="control-group">
|
||||||
<hr>
|
<div class="controls">
|
||||||
<label class="control-label"><i class="icon-wrench"></i> {{ _('Tools') }}</label>
|
<label class="control-label"><i class="icon-wrench"></i> {{ _('Tools') }}</label>
|
||||||
<button class="btn btn-block" data-bind="click: showLevelingDialog, enable: isActive()">{{ _('Assisted Bed Leveling') }}</button>
|
<button class="btn btn-block btn-small" data-bind="click: showLevelingDialog, enable: isActive()">{{ _('Assisted Bed Leveling') }}</button>
|
||||||
<button class="btn btn-block" data-bind="click: showPidTuningDialog, enable: isActive()">{{ _('PID Tuning') }}</button>
|
<button class="btn btn-block btn-small" data-bind="click: showPidTuningDialog, enable: isActive()">{{ _('PID Tuning') }}</button>
|
||||||
<button class="btn btn-block" data-bind="click: showOffsetDialog, enable: isActive()">{{ _('Coordinate Offset') }}</button>
|
<button class="btn btn-block btn-small" data-bind="click: showOffsetDialog, enable: isActive()">{{ _('Coordinate Offset') }}</button>
|
||||||
|
<button class="btn btn-block btn-small" data-bind="click: showGraphDialog, enable: isActive()">{{ _('Performance Graph') }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<hr>
|
|
||||||
<label class="control-label"><i class="icon-list-alt"></i> {{ _('Macros') }}</label>
|
<label class="control-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 class="btn btn-block" data-bind="text: name, click: $parent.executeMacro, enable: $parent.isActive()"></button>
|
<button class="btn btn-block btn-small" data-bind="text: name, click: $parent.executeMacro, enable: $parent.isActive()"></button>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue