1692 lines
54 KiB
JavaScript
1692 lines
54 KiB
JavaScript
// Main javascript for for Klippy Web Server Example
|
|
//
|
|
// Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
|
|
//
|
|
// This file may be distributed under the terms of the GNU GPLv3 license
|
|
|
|
import JsonRPC from "./json-rpc.js?v=0.1.2";
|
|
|
|
var origin = location.origin;
|
|
var ws_url = (window.location.protocol == "https" ? "wss://" : "ws://")
|
|
+ location.host;
|
|
|
|
// API Definitions
|
|
var api = {
|
|
printer_info: {
|
|
url: "/printer/info",
|
|
method: "printer.info"
|
|
},
|
|
gcode_script: {
|
|
url: "/printer/gcode/script",
|
|
method: "printer.gcode.script"
|
|
},
|
|
gcode_help: {
|
|
url: "/printer/gcode/help",
|
|
method: "printer.gcode.help"
|
|
},
|
|
start_print: {
|
|
url: "/printer/print/start",
|
|
method: "printer.print.start"
|
|
},
|
|
cancel_print: {
|
|
url: "/printer/print/cancel",
|
|
method: "printer.print.cancel"
|
|
},
|
|
pause_print: {
|
|
url: "/printer/print/pause",
|
|
method: "printer.print.pause"
|
|
},
|
|
resume_print: {
|
|
url: "/printer/print/resume",
|
|
method: "printer.print.resume"
|
|
},
|
|
query_endstops: {
|
|
url: "/printer/query_endstops/status",
|
|
method: "printer.query_endstops.status"
|
|
},
|
|
object_list: {
|
|
url: "/printer/objects/list",
|
|
method: "printer.objects.list"
|
|
},
|
|
object_status: {
|
|
url: "/printer/objects/query",
|
|
method: "printer.objects.query"
|
|
},
|
|
object_subscription: {
|
|
url: "/printer/objects/subscribe",
|
|
method: "printer.objects.subscribe"
|
|
},
|
|
temperature_store: {
|
|
url: "/server/temperature_store",
|
|
method: "server.temperature_store"
|
|
},
|
|
estop: {
|
|
url: "/printer/emergency_stop",
|
|
method: "printer.emergency_stop"
|
|
},
|
|
restart: {
|
|
url: "/printer/restart",
|
|
method: "printer.restart"
|
|
},
|
|
firmware_restart: {
|
|
url: "/printer/firmware_restart",
|
|
method: "printer.firmware_restart"
|
|
},
|
|
|
|
// File Management Apis
|
|
file_list:{
|
|
url: "/server/files/list",
|
|
method: "server.files.list"
|
|
},
|
|
metadata: {
|
|
url: "/server/files/metadata",
|
|
method: "server.files.metadata"
|
|
},
|
|
directory: {
|
|
url: "/server/files/directory",
|
|
method: {
|
|
get: "server.files.get_directory",
|
|
post: "server.files.post_directory",
|
|
delete: "server.files.delete_directory"
|
|
}
|
|
},
|
|
move: {
|
|
url: "/server/files/move",
|
|
method: "server.files.move"
|
|
},
|
|
copy: {
|
|
url: "/server/files/copy",
|
|
method: "server.files.copy"
|
|
},
|
|
delete_file: {
|
|
method: "server.files.delete_file"
|
|
},
|
|
upload: {
|
|
url: "/server/files/upload"
|
|
},
|
|
gcode_files: {
|
|
url: "/server/files/gcodes/"
|
|
},
|
|
klippy_log: {
|
|
url: "/server/files/klippy.log"
|
|
},
|
|
moonraker_log: {
|
|
url: "/server/files/moonraker.log"
|
|
},
|
|
cfg_files: {
|
|
url: "/server/files/config/"
|
|
},
|
|
cfg_examples: {
|
|
url: "/server/files/config_examples/"
|
|
},
|
|
|
|
// Machine APIs
|
|
reboot: {
|
|
url: "/machine/reboot",
|
|
method: "machine.reboot"
|
|
},
|
|
shutdown: {
|
|
url: "/machine/shutdown",
|
|
method: "machine.shutdown"
|
|
},
|
|
|
|
// Access APIs
|
|
apikey: {
|
|
url: "/access/api_key"
|
|
},
|
|
oneshot_token: {
|
|
url: "/access/oneshot_token"
|
|
}
|
|
}
|
|
|
|
var websocket = null;
|
|
var apikey = null;
|
|
var paused = false;
|
|
var klippy_ready = false;
|
|
var api_type = 'http';
|
|
var is_printing = false;
|
|
var json_rpc = new JsonRPC();
|
|
|
|
function round_float (value) {
|
|
if (typeof value == "number" && !Number.isInteger(value)) {
|
|
return value.toFixed(2);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
//****************UI Update Functions****************/
|
|
var line_count = 0;
|
|
function update_term(msg) {
|
|
let start = '<div id="line' + line_count + '">';
|
|
$("#term").append(start + msg + "</div>");
|
|
line_count++;
|
|
if (line_count >= 50) {
|
|
let rm = line_count - 50
|
|
$("#line" + rm).remove();
|
|
}
|
|
if ($("#cbxAuto").is(":checked")) {
|
|
$("#term").stop().animate({
|
|
scrollTop: $("#term")[0].scrollHeight
|
|
}, 800);
|
|
}
|
|
}
|
|
|
|
const max_stream_div_width = 5;
|
|
var stream_div_width = max_stream_div_width;
|
|
var stream_div_height = 0;
|
|
function update_streamdiv(obj, attr, val) {
|
|
if (stream_div_width >= max_stream_div_width) {
|
|
stream_div_height++;
|
|
stream_div_width = 0;
|
|
$('#streamdiv').append("<div id='sdrow" + stream_div_height +
|
|
"' style='display: flex'></div>");
|
|
}
|
|
let id = obj.replace(/\s/g, "_") + "_" + attr;
|
|
if ($("#" + id).length == 0) {
|
|
$('#sdrow' + stream_div_height).append("<div style='width: 10em; border: 2px solid black'>"
|
|
+ obj + " " + attr + ":<div id='" + id + "'></div></div>");
|
|
stream_div_width++;
|
|
}
|
|
|
|
let out = "";
|
|
if (val instanceof Array) {
|
|
val.forEach((value, idx, array) => {
|
|
out += round_float(value);
|
|
if (idx < array.length -1) {
|
|
out += ", "
|
|
}
|
|
});
|
|
} else {
|
|
out = round_float(val);
|
|
}
|
|
$("#" + id).text(out);
|
|
}
|
|
|
|
var last_progress = 0;
|
|
function update_progress(loaded, total) {
|
|
let progress = parseInt(loaded / total * 100);
|
|
if (progress - last_progress > 1 || progress >= 100) {
|
|
if (progress >= 100) {
|
|
last_progress = 0;
|
|
progress = 100;
|
|
console.log("File transfer complete")
|
|
} else {
|
|
last_progress = progress;
|
|
}
|
|
$('#upload_progress').text(progress);
|
|
$('#progressbar').val(progress);
|
|
}
|
|
}
|
|
|
|
function update_error(cmd, msg) {
|
|
if (msg instanceof Object)
|
|
msg = JSON.stringify(msg);
|
|
// Handle server error responses
|
|
update_term("Command [" + cmd + "] resulted in an error: " + msg);
|
|
console.log("Error processing " + cmd +": " + msg);
|
|
}
|
|
|
|
function handle_klippy_state(state) {
|
|
// Klippy state can be "ready", "disconnect", and "shutdown". This
|
|
// differs from Printer State in that it represents the status of
|
|
// the Host software
|
|
switch(state) {
|
|
case "ready":
|
|
// It would be possible to use this event to notify the
|
|
// client that the printer has started, however the server
|
|
// may not start in time for clients to receive this event.
|
|
// It is being kept in case
|
|
if (!klippy_ready)
|
|
update_term("Klippy Ready");
|
|
break;
|
|
case "shutdown":
|
|
// Either M112 was entered or there was a printer error. We
|
|
// probably want to notify the user and disable certain controls.
|
|
klippy_ready = false;
|
|
update_term("Klipper has shutdown, check klippy.log for info");
|
|
break;
|
|
}
|
|
}
|
|
|
|
function process_mesh(result) {
|
|
let bed_mesh = result.status.bed_mesh;
|
|
let matrix = bed_mesh.probed_matrix;
|
|
if (!(matrix instanceof Array) || matrix.length < 3 ||
|
|
!(matrix[0] instanceof Array) || matrix[0].length < 3) {
|
|
// make sure that the matrix is valid
|
|
console.log("Invalid Mesh Received");
|
|
return;
|
|
}
|
|
let coordinates = [];
|
|
let x_distance = (bed_mesh.mesh_max[0] - bed_mesh.mesh_min[0]) /
|
|
(matrix[0].length - 1);
|
|
let y_distance = (bed_mesh.mesh_max[1] - bed_mesh.mesh_min[1]) /
|
|
(matrix.length - 1);
|
|
let x_idx = 0;
|
|
let y_idx = 0;
|
|
for (const x_axis of matrix) {
|
|
x_idx = 0;
|
|
let y_coord = bed_mesh.mesh_min[1] + (y_idx * y_distance);
|
|
for (const z_coord of x_axis) {
|
|
let x_coord = bed_mesh.mesh_min[0] + (x_idx * x_distance);
|
|
x_idx++;
|
|
coordinates.push([x_coord, y_coord, z_coord]);
|
|
}
|
|
y_idx++;
|
|
}
|
|
console.log("Processed Mesh Coordinates:");
|
|
console.log(coordinates);
|
|
}
|
|
//***********End UI Update Functions****************/
|
|
|
|
//***********Websocket-Klipper API Functions (JSON-RPC)************/
|
|
function get_file_list(root) {
|
|
let args = {root: root}
|
|
json_rpc.call_method_with_kwargs(api.file_list.method, args)
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgment that the gcode has
|
|
// been successfully processed
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.file_list.method, error);
|
|
});
|
|
}
|
|
|
|
function get_klippy_info() {
|
|
// A "get_klippy_info" websocket request. It returns
|
|
// the hostname (which should be equal to location.host), the
|
|
// build version, and if the Host is ready for commands. Its a
|
|
// good idea to fetch this information after the websocket connects.
|
|
// If the Host is in a "ready" state, we can do some initialization
|
|
json_rpc.call_method(api.printer_info.method)
|
|
.then((result) => {
|
|
|
|
if (result.state == "ready") {
|
|
if (!klippy_ready) {
|
|
update_term("Klippy Hostname: " + result.hostname +
|
|
" | CPU: " + result.cpu_info +
|
|
" | Build Version: " + result.software_version);
|
|
klippy_ready = true;
|
|
|
|
// Add our subscriptions the the UI is configured to do so.
|
|
if ($("#cbxSub").is(":checked")) {
|
|
// If autosubscribe is check, request the subscription now
|
|
const sub = {
|
|
objects: {
|
|
gcode_move: ["gcode_position", "speed", "speed_factor", "extrude_factor"],
|
|
idle_timeout: null,
|
|
pause_resume: null,
|
|
toolhead: null,
|
|
virtual_sdcard: null,
|
|
heater_bed: null,
|
|
extruder: ["temperature", "target"],
|
|
fan: null,
|
|
print_stats: null}
|
|
};
|
|
add_subscription(sub);
|
|
} else {
|
|
get_status({idle_timeout: null, pause_resume: null});
|
|
}
|
|
}
|
|
} else {
|
|
if (result.state == "error") {
|
|
update_term(result.state_message);
|
|
} else {
|
|
update_term("Waiting for Klippy ready status...");
|
|
}
|
|
console.log("Klippy Not Ready, checking again in 2s: ");
|
|
setTimeout(() => {
|
|
get_klippy_info();
|
|
}, 2000);
|
|
}
|
|
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.printer_info.method, error);
|
|
setTimeout(() => {
|
|
get_klippy_info();
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function run_gcode(gcode) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.gcode_script.method, {script: gcode})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgment that the gcode has
|
|
// been successfully processed
|
|
update_term(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.gcode_script.method, error);
|
|
});
|
|
}
|
|
|
|
function get_status(printer_objects) {
|
|
// Note that this is just an example of one particular use of get_status.
|
|
// In a robust client you would likely pass a callback to this function
|
|
// so that you can respond to various status requests. It would also
|
|
// be possible to subscribe to status requests and update the UI accordingly
|
|
json_rpc.call_method_with_kwargs(api.object_status.method, printer_objects)
|
|
.then((result) => {
|
|
if ("idle_timeout" in result) {
|
|
// Its a good idea that the user understands that some functionality,
|
|
// such as file manipulation, is disabled during a print. This can be
|
|
// done by disabling buttons or by notifying the user via a popup if they
|
|
// click on an action that is not allowed.
|
|
if ("state" in result.idle_timeout) {
|
|
let state = result.idle_timeout.state.toLowerCase();
|
|
is_printing = (state == "printing");
|
|
if (!$('#cbxFileTransfer').is(":checked")) {
|
|
$('.toggleable').prop(
|
|
'disabled', (api_type == 'websocket' || is_printing));
|
|
}
|
|
$('#btnstartprint').prop('disabled', is_printing);
|
|
}
|
|
}
|
|
if ("pause_resume" in result) {
|
|
if ("is_paused" in result.pause_resume) {
|
|
paused = result.pause_resume.is_paused;
|
|
let label = paused ? "Resume Print" : "Pause Print";
|
|
$('#btnpauseresume').text(label);
|
|
}
|
|
}
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.object_status.method, error);
|
|
});
|
|
}
|
|
|
|
function get_object_list() {
|
|
json_rpc.call_method(api.object_list.method)
|
|
.then((result) => {
|
|
// result will be a dictionary containing all available printer
|
|
// objects available for query or subscription
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.object_list.method, error);
|
|
});
|
|
}
|
|
|
|
function get_mesh() {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.object_status.method, {objects: {bed_mesh: null}})
|
|
.then((result) => {
|
|
process_mesh(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.object_status.method, error);
|
|
});
|
|
}
|
|
|
|
function add_subscription(printer_objects) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.object_subscription.method, printer_objects)
|
|
.then((result) => {
|
|
// result is the the state from all fetched data
|
|
handle_status_update(result.status)
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.object_subscription.method, error);
|
|
});
|
|
}
|
|
|
|
function get_subscribed() {
|
|
json_rpc.call_method(api.object_subscription.method)
|
|
.then((result) => {
|
|
// result is a dictionary containing all currently subscribed
|
|
// printer objects/attributes
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.object_subscription.method, error);
|
|
});
|
|
}
|
|
|
|
function get_endstops() {
|
|
json_rpc.call_method(api.query_endstops.method)
|
|
.then((result) => {
|
|
// A response to a "get_endstops" websocket request.
|
|
// The result contains an object of key/value pairs,
|
|
// where the key is the endstop (ie:x, y, or z) and the
|
|
// value is either "open" or "TRIGGERED".
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.query_endstops.method, error);
|
|
});
|
|
}
|
|
|
|
function start_print(file_name) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.start_print.method, {'filename': file_name})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.start_print.method, error);
|
|
});
|
|
}
|
|
|
|
function cancel_print() {
|
|
json_rpc.call_method(api.cancel_print.method)
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has been canceled
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.cancel_print.method, error);
|
|
});
|
|
}
|
|
|
|
function pause_print() {
|
|
json_rpc.call_method(api.pause_print.method)
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has been paused
|
|
console.log("Pause Command Executed")
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.pause_print.method, error);
|
|
});
|
|
}
|
|
|
|
function resume_print() {
|
|
json_rpc.call_method(api.resume_print.method)
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has been resumed
|
|
console.log("Resume Command Executed")
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.resume_print.method, error);
|
|
});
|
|
}
|
|
|
|
function get_metadata(file_name) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.metadata.method, {'filename': file_name})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.metadata.method, error);
|
|
});
|
|
}
|
|
|
|
function make_directory(dir_path) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.directory.method.post, {'path': dir_path})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.directory.method.post, error);
|
|
});
|
|
}
|
|
|
|
function delete_directory(dir_path) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.directory.method.delete,
|
|
{'path': dir_path, 'force': true})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.directory.method.delete, error);
|
|
});
|
|
}
|
|
|
|
function delete_file(file_path) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.delete_file.method, {'path': file_path})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.delete_file.method, error);
|
|
});
|
|
}
|
|
|
|
function copy_item(source_path, dest_path) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.copy.method, {'source': source_path, 'dest': dest_path})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.copy.method, error);
|
|
});
|
|
}
|
|
|
|
function move_item(source_path, dest_path) {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.move.method, {'source': source_path, 'dest': dest_path})
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has started
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.move.method, error);
|
|
});
|
|
}
|
|
|
|
function get_gcode_help() {
|
|
json_rpc.call_method(api.gcode_help.method)
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has been resumed
|
|
console.log(result)
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.gcode_help.method, error);
|
|
});
|
|
}
|
|
|
|
function emergency_stop() {
|
|
json_rpc.call_method(api.estop.method)
|
|
.then((result) => {
|
|
// result is an "ok" acknowledgement that the
|
|
// print has been resumed
|
|
console.log(result)
|
|
})
|
|
.catch((error) => {
|
|
update_error(api.estop.method, error);
|
|
});
|
|
}
|
|
|
|
function restart() {
|
|
// We are unlikely to receive a response from a restart
|
|
// request as the websocket will disconnect, so we will
|
|
// call json_rpc.notify instead of call_function.
|
|
json_rpc.notify(api.restart.method);
|
|
}
|
|
|
|
function firmware_restart() {
|
|
// As above, we would not likely receive a response from
|
|
// a firmware_restart request
|
|
json_rpc.notify(api.firmware_restart.method);
|
|
}
|
|
|
|
function reboot() {
|
|
json_rpc.notify(api.reboot.method);
|
|
}
|
|
|
|
function shutdown() {
|
|
json_rpc.notify(api.shutdown.method);
|
|
}
|
|
|
|
//***********End Websocket-Klipper API Functions (JSON-RPC)********/
|
|
|
|
//***********Klipper Event Handlers (JSON-RPC)*********************/
|
|
|
|
function handle_gcode_response(response) {
|
|
// This event contains all gcode responses that would
|
|
// typically be printed to the terminal. Its possible
|
|
// That multiple lines can be bundled in one response,
|
|
// so if displaying we want to be sure we split them.
|
|
let messages = response.split("\n");
|
|
for (let msg of messages) {
|
|
update_term(msg);
|
|
}
|
|
}
|
|
json_rpc.register_method("notify_gcode_response", handle_gcode_response);
|
|
|
|
function handle_status_update(status) {
|
|
// This is subscribed status data. Here we do a nested
|
|
// for-each to determine the klippy object name ("name"),
|
|
// the attribute we want ("attr"), and the attribute's
|
|
// value ("val")
|
|
for (let name in status) {
|
|
let obj = status[name];
|
|
for (let attr in obj) {
|
|
let full_name = name + "." + attr;
|
|
let val = obj[attr];
|
|
switch(full_name) {
|
|
case "print_stats.filename":
|
|
$('#filename').prop("hidden", val == "");
|
|
$('#filename').text("Loaded File: " + val);
|
|
break;
|
|
case "pause_resume.is_paused":
|
|
if (paused != val) {
|
|
paused = val;
|
|
let label = paused ? "Resume Print" : "Pause Print";
|
|
$('#btnpauseresume').text(label);
|
|
console.log("Paused State Changed: " + val);
|
|
update_streamdiv(name, attr, val);
|
|
}
|
|
break;
|
|
case "idle_timeout.state":
|
|
let state = val.toLowerCase();
|
|
if (state != is_printing) {
|
|
is_printing = (state == "printing");
|
|
if (!$('#cbxFileTransfer').is(":checked")) {
|
|
$('.toggleable').prop(
|
|
'disabled', (api_type == 'websocket' || is_printing));
|
|
}
|
|
$('#btnstartprint').prop('disabled', is_printing);
|
|
update_streamdiv(name, attr, val);
|
|
}
|
|
break;
|
|
case "webhooks.state":
|
|
handle_klippy_state(val);
|
|
default:
|
|
update_streamdiv(name, attr, val);
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
json_rpc.register_method("notify_status_update", handle_status_update);
|
|
|
|
function handle_klippy_disconnected() {
|
|
// Klippy has disconnected from the MCU and is prepping to
|
|
// restart. The client will receive this signal right before
|
|
// the websocket disconnects. If we need to do any kind of
|
|
// cleanup on the client to prepare for restart this would
|
|
// be a good place.
|
|
klippy_ready = false;
|
|
update_term("Klippy Disconnected");
|
|
setTimeout(() => {
|
|
get_klippy_info();
|
|
}, 2000);
|
|
}
|
|
json_rpc.register_method("notify_klippy_disconnected", handle_klippy_disconnected);
|
|
|
|
function handle_file_list_changed(file_info) {
|
|
// This event fires when a client has either added or removed
|
|
// a gcode file.
|
|
// Update the jstree based on the action and info
|
|
let parent_node = parse_node_path(file_info.item.root, file_info.item.path);
|
|
$("#filelist").jstree('refresh_node', parent_node);
|
|
if (file_info.action == "move_item") {
|
|
let src_parent_node = parse_node_path(
|
|
file_info.source_item.root, file_info.source_item.path)
|
|
if (src_parent_node != parent_node)
|
|
$("#filelist").jstree('refresh_node', src_parent_node);
|
|
}
|
|
console.log("Filelist Changed:");
|
|
console.log(file_info);
|
|
}
|
|
json_rpc.register_method("notify_filelist_changed", handle_file_list_changed);
|
|
|
|
function handle_metadata_update(metadata) {
|
|
console.log(metadata);
|
|
}
|
|
json_rpc.register_method("notify_metadata_update", handle_metadata_update);
|
|
|
|
//***********End Klipper Event Handlers (JSON-RPC)*****************/
|
|
|
|
//*****************Websocket Batch GCode Tests*********************/
|
|
|
|
// The function below is an example of one way to use JSON-RPC's batch send
|
|
// method. Generally speaking it is better and easier to use individual
|
|
// requests, as matching requests with responses in a batch requires more
|
|
// work from the developer
|
|
function send_gcode_batch(gcodes) {
|
|
// The purpose of this function is to provide an example of a JSON-RPC
|
|
// "batch request". This function takes an array of gcodes and sends
|
|
// them as a batch command. This would behave like a Klipper Gcode Macro
|
|
// with one signficant difference...if one gcode in the batch requests
|
|
// results in an error, Klipper will continue to process subsequent gcodes.
|
|
// A Klipper Gcode Macro will immediately stop execution of the macro
|
|
// if an error is encountered.
|
|
|
|
let batch = [];
|
|
for (let gc of gcodes) {
|
|
batch.push(
|
|
{
|
|
method: api.gcode_script.method,
|
|
type: 'request',
|
|
params: {script: gc}
|
|
});
|
|
}
|
|
|
|
// The batch request returns a promise with all results
|
|
json_rpc.send_batch_request(batch)
|
|
.then((results) => {
|
|
for (let res of results) {
|
|
// Each result is an object with three keys:
|
|
// method: The method executed for this result
|
|
// index: The index of the original request
|
|
// result: The successful result
|
|
|
|
|
|
// Use the index to look up the gcode parameter in the original
|
|
// request
|
|
let orig_gcode = batch[res.index].params[0];
|
|
console.log("Batch Gcode " + orig_gcode +
|
|
" successfully executed with result: " + res.result);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
// Like the result, the error is an object. However there
|
|
// is an "error" in place of the "result key"
|
|
let orig_gcode = batch[err.index].params[0];
|
|
console.log("Batch Gcode <" + orig_gcode +
|
|
"> failed with error: " + err.error.message);
|
|
});
|
|
}
|
|
|
|
// The function below demonstrates a more useful method of sending
|
|
// a client side gcode macro. Like a Klipper macro, gcode execution
|
|
// will stop immediately upon encountering an error. The advantage
|
|
// of a client supporting their own macros is that there is no need
|
|
// to restart the klipper host after creating or deleting them.
|
|
async function send_gcode_macro(gcodes) {
|
|
for (let gc of gcodes) {
|
|
try {
|
|
let result = await json_rpc.call_method_with_kwargs(
|
|
api.gcode_script.method, {script: gc});
|
|
} catch (err) {
|
|
console.log("Error executing gcode macro: " + err.message);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//**************End Websocket Batch GCode Tests********************/
|
|
|
|
//*****************HTTP Helper Functions***************************/
|
|
|
|
function encode_filename(path) {
|
|
let parts = path.split("/")
|
|
if (!parts.length) {
|
|
return "";
|
|
}
|
|
let fname = encodeURIComponent(parts.pop())
|
|
if (parts.length) {
|
|
return parts.join("/") + "/" + fname;
|
|
}
|
|
return fname;
|
|
}
|
|
function form_get_request(api_url, query_string="") {
|
|
let settings = {url: origin + api_url + query_string};
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.get(settings, (resp, status) => {
|
|
console.log(resp);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function form_post_request(api_url, query_string="") {
|
|
let settings = {url: origin + api_url + query_string};
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.post(settings, (resp, status) => {
|
|
console.log(resp);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function form_delete_request(api_url, query_string="") {
|
|
let settings = {
|
|
url: origin + api_url + query_string,
|
|
method: 'DELETE',
|
|
success: (resp, status) => {
|
|
console.log(resp);
|
|
return false;
|
|
}
|
|
};
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.ajax(settings);
|
|
}
|
|
|
|
function form_download_request(uri) {
|
|
let dl_url = origin + uri;
|
|
if (apikey != null) {
|
|
let settings = {
|
|
url: origin + api.oneshot_token.url,
|
|
headers: {"X-Api-Key": apikey}
|
|
};
|
|
$.get(settings, (resp, status) => {
|
|
let token = resp.result;
|
|
dl_url += "?token=" + token;
|
|
do_download(dl_url);
|
|
return false;
|
|
});
|
|
} else {
|
|
do_download(dl_url);
|
|
}
|
|
}
|
|
|
|
//*************End HTTP Helper Functions***************************/
|
|
|
|
//***************JSTree Helper Functions***************************/
|
|
|
|
function parse_node_path(root, path) {
|
|
let slice_idx = path.lastIndexOf("/");
|
|
let node_path = "";
|
|
if (slice_idx != -1)
|
|
node_path = "/" + path.slice(0, slice_idx);
|
|
return root + node_path;
|
|
}
|
|
|
|
function get_selected_node() {
|
|
let js_instance = $("#filelist").jstree(true);
|
|
let sel = js_instance.get_selected();
|
|
if (!sel.length)
|
|
return null;
|
|
sel = sel[0];
|
|
return js_instance.get_node(sel);
|
|
}
|
|
|
|
function get_selected_item(type="file") {
|
|
let node = get_selected_node();
|
|
if (node == null || node.type != type)
|
|
return "";
|
|
return node.id;
|
|
}
|
|
|
|
function generate_children(result, parent) {
|
|
let children = [];
|
|
result.dirs.sort((a, b) => {
|
|
return a.dirname > b.dirname ? 1 : -1;
|
|
})
|
|
result.files.sort((a, b) => {
|
|
return a.filename > b.filename ? 1 : -1;
|
|
});
|
|
for (let dir of result.dirs) {
|
|
let full_path = parent.id + "/" + dir.dirname;
|
|
children.push({text: dir.dirname, id: full_path,
|
|
type: "dir", children: true,
|
|
mutable: parent.original.mutable});
|
|
}
|
|
for (let file of result.files) {
|
|
let full_path = parent.id + "/" + file.filename;
|
|
children.push({text: file.filename, id: full_path,
|
|
type: "file", mutable: parent.original.mutable});
|
|
}
|
|
return children;
|
|
}
|
|
|
|
function jstree_populate_children(node, callback) {
|
|
if (api_type == "http") {
|
|
let qs = `?path=${node.id}`;
|
|
let settings = {url: origin + api.directory.url + qs};
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.get(settings, (resp, status) => {
|
|
callback(generate_children(resp.result, node));
|
|
return false;
|
|
});
|
|
} else {
|
|
json_rpc.call_method_with_kwargs(
|
|
api.directory.method.get, {path: node.id})
|
|
.then((result) => {
|
|
callback(generate_children(result, node));
|
|
})
|
|
.catch((error) => {
|
|
console.log(error)
|
|
update_error(api.directory.method.get, error);
|
|
});
|
|
}
|
|
}
|
|
|
|
function jstree_download_file() {
|
|
update_progress(0, 100);
|
|
let filename = get_selected_item();
|
|
if (filename) {
|
|
let url = `/server/files/${encode_filename(filename)}`;
|
|
form_download_request(url);
|
|
}
|
|
}
|
|
|
|
var paste_item = null;
|
|
function jstree_paste() {
|
|
if (paste_item == null) {
|
|
console.log(`Invalid Paste Command`);
|
|
return
|
|
}
|
|
let node = get_selected_node()
|
|
if (node == null || node == paste_item.source_node) {
|
|
paste_item = null;
|
|
return;
|
|
}
|
|
let source_path = paste_item.source_node.id;
|
|
let dest_path = node.id;
|
|
// TODO: Need checks here.
|
|
// - If the source is a file and the action is move, the destination must
|
|
// - be a directory?
|
|
if (paste_item.source_node.id == node.id) {
|
|
// Can't move or copy to the same item
|
|
return;
|
|
} else if (paste_item.source_node.type != "file" && node.type == "file") {
|
|
// Can't copy or move a directory into a file
|
|
return;
|
|
} else if (paste_item.action == "copy" && node.type != "file") {
|
|
// When copying to a folder add the file/folder name to the destination
|
|
dest_path += `/${paste_item.source_node.text}`;
|
|
}
|
|
if (api_type == 'http') {
|
|
let api_url = paste_item.action == "move" ? api.move.url : api.copy.url;
|
|
let qs = `?source=${source_path}&dest=${dest_path}`;
|
|
form_post_request(api_url, qs);
|
|
} else {
|
|
// Websocket
|
|
if (paste_item.action == "move")
|
|
move_item(source_path, dest_path);
|
|
else
|
|
copy_item(source_path, dest_path);
|
|
}
|
|
paste_item = null;
|
|
}
|
|
|
|
function jstree_delete_item() {
|
|
let node = get_selected_node();
|
|
if (node == null) {
|
|
console.log("Invalid item selection, cannot delete");
|
|
}
|
|
if (api_type == 'http') {
|
|
let api_url;
|
|
let qs = "";
|
|
if (node.type == "file") {
|
|
api_url = `/server/files/${encode_filename(node.id)}`;
|
|
} else {
|
|
api_url = api.directory.url;
|
|
qs = `?path=${encode_filename(node.id)}&force=true`;
|
|
}
|
|
form_delete_request(api_url, qs);
|
|
} else {
|
|
if (node.type == "file")
|
|
delete_file(node.id);
|
|
else
|
|
delete_directory(node.id);
|
|
}
|
|
}
|
|
|
|
function jstree_new_folder(node, status, cancelled) {
|
|
if (!status)
|
|
return;
|
|
let instance = $("#filelist").jstree(true);
|
|
if (cancelled) {
|
|
console.log("Create Folder Cancelled")
|
|
instance.delete_node(node);
|
|
return;
|
|
}
|
|
console.log(`Create Folder: ${node}`);
|
|
let parent = instance.get_node(instance.get_parent(node));
|
|
if (parent.type == "file") {
|
|
console.log("Invalid folder, cannot create")
|
|
instance.delete_node(node);
|
|
return;
|
|
}
|
|
let path = parent.id + "/" + node.text;
|
|
if (api_type == 'http') {
|
|
let url = api.directory.url;
|
|
let qs = "?path=" + path;
|
|
form_post_request(url, qs);
|
|
|
|
} else {
|
|
make_directory(path);
|
|
}
|
|
}
|
|
|
|
function jstree_rename(node, status, cancelled) {
|
|
if (!status || cancelled)
|
|
return;
|
|
let source_path = node.id;
|
|
let instance = $("#filelist").jstree(true);
|
|
let dest_path = instance.get_parent(node) + "/" + node.text;
|
|
if (api_type == 'http') {
|
|
let qs = `?source=${source_path}&dest=${dest_path}`;
|
|
form_post_request(api.move.url, qs);
|
|
} else {
|
|
move_item(source_path, dest_path);
|
|
}
|
|
}
|
|
|
|
function jstree_start_print() {
|
|
let filename = get_selected_item();
|
|
if (filename && filename.startsWith("gcodes/")) {
|
|
filename = filename.slice(7)
|
|
let qs = `?filename=${encode_filename(filename)}`;
|
|
if (api_type == 'http') {
|
|
form_post_request(api.start_print.url, qs);
|
|
} else {
|
|
start_print(filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
function jstree_get_metadata() {
|
|
let filename = get_selected_item();
|
|
if (filename && filename.startsWith("gcodes/")) {
|
|
filename = filename.slice(7);
|
|
if (api_type == 'http') {
|
|
let qs = `?filename=${encode_filename(filename)}`;
|
|
form_get_request(api.metadata.url, qs);
|
|
} else {
|
|
get_metadata(filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
//***********End JSTree Helper Functions***************************/
|
|
|
|
// A simple reconnecting websocket
|
|
class KlippyWebsocket {
|
|
constructor(addr) {
|
|
this.base_address = addr;
|
|
this.connected = false;
|
|
this.ws = null;
|
|
this.onmessage = null;
|
|
this.onopen = null;
|
|
this.connect();
|
|
}
|
|
|
|
connect() {
|
|
// Doing the websocket connection here allows the websocket
|
|
// to reconnect if its closed. This is nice as it allows the
|
|
// client to easily recover from Klippy restarts without user
|
|
// intervention
|
|
if (apikey != null) {
|
|
// Fetch a oneshot token to pass websocket authorization
|
|
let token_settings = {
|
|
url: origin + api.oneshot_token.url,
|
|
headers: {
|
|
"X-Api-Key": apikey
|
|
}
|
|
}
|
|
$.get(token_settings, (data, status) => {
|
|
let token = data.result;
|
|
let url = this.base_address + "/websocket?token=" + token;
|
|
this.ws = new WebSocket(url);
|
|
this._set_callbacks();
|
|
}).fail(() => {
|
|
console.log("Failed to retreive oneshot token");
|
|
})
|
|
} else {
|
|
this.ws = new WebSocket(this.base_address + "/websocket");
|
|
this._set_callbacks();
|
|
}
|
|
}
|
|
|
|
_set_callbacks() {
|
|
this.ws.onopen = () => {
|
|
this.connected = true;
|
|
console.log("Websocket connected");
|
|
if (this.onopen != null)
|
|
this.onopen();
|
|
};
|
|
|
|
this.ws.onclose = (e) => {
|
|
klippy_ready = false;
|
|
this.connected = false;
|
|
console.log("Websocket Closed, reconnecting in 1s: ", e.reason);
|
|
setTimeout(() => {
|
|
this.connect();
|
|
}, 1000);
|
|
};
|
|
|
|
this.ws.onerror = (err) => {
|
|
klippy_ready = false;
|
|
console.log("Websocket Error: ", err.message);
|
|
this.ws.close();
|
|
};
|
|
|
|
this.ws.onmessage = (e) => {
|
|
// Tornado Server Websockets support text encoded frames.
|
|
// The onmessage callback will send the data straight to
|
|
// JSON-RPC
|
|
this.onmessage(e.data);
|
|
};
|
|
}
|
|
|
|
send(data) {
|
|
// Only allow send if connected
|
|
if (this.connected) {
|
|
this.ws.send(data);
|
|
} else {
|
|
console.log("Websocket closed, cannot send data");
|
|
}
|
|
}
|
|
|
|
close() {
|
|
// TODO: Cancel the timeout
|
|
this.ws.close();
|
|
}
|
|
|
|
};
|
|
|
|
function create_websocket(url) {
|
|
if (websocket != null)
|
|
websocket.close()
|
|
websocket = new KlippyWebsocket(ws_url);
|
|
websocket.onopen = () => {
|
|
// Depending on the state of the printer, all enpoints may not be
|
|
// available when the websocket is first opened. The "get_klippy_info"
|
|
// method is available, and should be used to determine if Klipper is
|
|
// in the "ready" state. When Klipper is "ready", all endpoints should
|
|
// be registered and available.
|
|
|
|
// These could be implemented JSON RPC Batch requests and send both
|
|
// at the same time, however it is easier to simply do them
|
|
// individually
|
|
get_klippy_info();
|
|
};
|
|
json_rpc.register_transport(websocket);
|
|
}
|
|
|
|
function check_authorization() {
|
|
// send a HTTP "run gcode" command
|
|
let settings = {
|
|
url: origin + api.printer_info.url,
|
|
statusCode: {
|
|
401: function() {
|
|
// Show APIKey Popup
|
|
let result = window.prompt("Enter a valid API Key:", "");
|
|
if (result == null || result.length != 32) {
|
|
console.log("Invalid API Key: " + result);
|
|
apikey = null;
|
|
} else {
|
|
apikey = result;
|
|
}
|
|
check_authorization();
|
|
}
|
|
}
|
|
}
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.get(settings, (data, status) => {
|
|
// Create a websocket if /printer/info successfully returns
|
|
create_websocket();
|
|
})
|
|
}
|
|
|
|
function do_download(url) {
|
|
$('#hidden_link').attr('href', url);
|
|
$('#hidden_link')[0].click();
|
|
}
|
|
|
|
window.onload = () => {
|
|
// Handle changes between the HTTP and Websocket API
|
|
$('.reqws').prop('disabled', true);
|
|
$('input[type=radio][name=test_type]').on('change', function() {
|
|
api_type = $(this).val();
|
|
let disable_transfer = (!$('#cbxFileTransfer').is(":checked") && is_printing);
|
|
$('.toggleable').prop(
|
|
'disabled', (api_type == 'websocket' || disable_transfer));
|
|
$('.reqws').prop('disabled', (api_type == 'http'));
|
|
$('#apimethod').prop('hidden', (api_type == "websocket"));
|
|
$('#apiargs').prop('hidden', (api_type == "http"));
|
|
});
|
|
|
|
// Instantiate basic jstree
|
|
$('#filelist').jstree({
|
|
core: {
|
|
multiple: false,
|
|
check_callback: true,
|
|
data: function (node, cb) {
|
|
if (node.id === "#") {
|
|
cb([
|
|
{text: "gcodes", id: "gcodes",
|
|
type: "root", children: true, mutable: true},
|
|
{text: "config", id: "config",
|
|
type: "root", children: true, mutable: true},
|
|
{text: "config_examples", id: "config_examples",
|
|
type: "root", children: true, mutable: false}
|
|
]);
|
|
} else {
|
|
jstree_populate_children(node, cb);
|
|
}
|
|
}
|
|
},
|
|
types: {
|
|
default: {
|
|
icon: "jstree-folder"
|
|
},
|
|
"#": {
|
|
valid_children: ["root"],
|
|
max_children: 3
|
|
},
|
|
root: {
|
|
icon: "jstree-folder",
|
|
valid_children: ["dir", "file"]
|
|
},
|
|
file: {
|
|
icon: "jstree-file",
|
|
valid_children: []
|
|
},
|
|
dir: {
|
|
icon: "jstree-folder",
|
|
valid_children: ["dir", "file"]
|
|
}
|
|
},
|
|
contextmenu: {
|
|
items: (node, cb) => {
|
|
let actions = Object();
|
|
if (node.type != "file") {
|
|
// Can upload, can paste
|
|
actions.upload = {
|
|
label: "Upload File",
|
|
_disabled: !node.original.mutable,
|
|
action: () => {
|
|
$('#upload-file').click();
|
|
}
|
|
}
|
|
actions.new_folder = {
|
|
label: "New Folder",
|
|
_disabled: !node.original.mutable,
|
|
action: () => {
|
|
let instance = $("#filelist").jstree(true);
|
|
let par = get_selected_node();
|
|
if (par == null)
|
|
return false;
|
|
instance.create_node(par, {
|
|
type: "dir",
|
|
}, "first", (ch_node) => {
|
|
if (ch_node) {
|
|
let val = instance.edit(
|
|
ch_node.id, null, jstree_new_folder);
|
|
if (val === false) {
|
|
console.log(instance.last_error());
|
|
}
|
|
} else {
|
|
console.log("Invalid Child, cannot create dir");
|
|
}
|
|
});
|
|
|
|
},
|
|
separator_after: true
|
|
}
|
|
} else {
|
|
actions.download = {
|
|
label: "Download File",
|
|
action: jstree_download_file,
|
|
separator_after: true
|
|
}
|
|
}
|
|
if (node.type != "root") {
|
|
if (node.type == "file" && node.id.startsWith("gcodes")) {
|
|
actions.print = {
|
|
label: "Start Print",
|
|
_disabled: is_printing,
|
|
action: jstree_start_print
|
|
}
|
|
actions.metadata = {
|
|
label: "Get Metadata",
|
|
separator_after: true,
|
|
action: jstree_get_metadata
|
|
}
|
|
}
|
|
// can delete, cut (move), copy, or rename(move)
|
|
actions.rename = {
|
|
label: "Rename",
|
|
_disabled: !node.original.mutable,
|
|
action: () => {
|
|
let instance = $("#filelist").jstree(true);
|
|
let cur = get_selected_node();
|
|
if (cur == null)
|
|
return false;
|
|
instance.edit(cur, null, jstree_rename);
|
|
}
|
|
}
|
|
actions.delete = {
|
|
label: "Delete",
|
|
_disabled: !node.original.mutable,
|
|
action: jstree_delete_item,
|
|
separator_after: true
|
|
}
|
|
actions.edit = {
|
|
label: "Edit",
|
|
submenu: {}
|
|
}
|
|
actions.edit.submenu.cut = {
|
|
label: "Cut",
|
|
_disabled: !node.original.mutable,
|
|
action: () => {
|
|
paste_item = {
|
|
action: "move",
|
|
source_node: node
|
|
}
|
|
}
|
|
}
|
|
actions.edit.submenu.copy = {
|
|
label: "Copy",
|
|
action: () => {
|
|
paste_item = {
|
|
action: "copy",
|
|
source_node: node
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!("edit" in actions)) {
|
|
actions.edit = {
|
|
label: "Edit",
|
|
submenu: {}
|
|
}
|
|
}
|
|
actions.edit.submenu.paste = {
|
|
label: "Paste",
|
|
_disabled: paste_item == null ||
|
|
paste_item.source_node.id == node.id ||
|
|
!node.original.mutable,
|
|
action: jstree_paste
|
|
}
|
|
cb(actions);
|
|
}
|
|
},
|
|
plugins: ["types", "contextmenu"]
|
|
});
|
|
|
|
$('#cbxFileTransfer').on('change', function () {
|
|
let disabled = false;
|
|
if (!$(this).is(":checked")) {
|
|
disabled = (api_type == 'websocket' || is_printing);
|
|
}
|
|
$('.toggleable').prop( 'disabled', disabled);
|
|
});
|
|
|
|
// Send a gcode. Note that in the test client nearly every control
|
|
// checks a radio button to see if the request should be sent via
|
|
// the REST API or the Websocket API. A real client will choose one
|
|
// or the other, so the "api_type" check will be unnecessary
|
|
$('#gcform').submit((evt) => {
|
|
let line = $('#gcform [type=text]').val();
|
|
$('#gcform [type=text]').val('');
|
|
update_term(line);
|
|
if (api_type == 'http') {
|
|
// send a HTTP "run gcode" command
|
|
let qs = "?script=" + line;
|
|
form_post_request(api.gcode_script.url, qs);
|
|
} else {
|
|
// Send a websocket "run gcode" command.
|
|
run_gcode(line);
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Send a command to the server. This can be either an HTTP
|
|
// get request formatted as the endpoint(ie: /objects) or
|
|
// a websocket command. The websocket command needs to be
|
|
// formatted as if it were already json encoded.
|
|
$('#apiform').submit((evt) => {
|
|
// Send to a user defined endpoint and log the response
|
|
if (api_type == 'http') {
|
|
let sendtype = $("input[type=radio][name=api_cmd_type]:checked").val();
|
|
let url = $('#apirequest').val();
|
|
let settings = {url: url}
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
if (sendtype == "get") {
|
|
console.log("Sending GET " + url);
|
|
form_get_request(url);
|
|
} else if (sendtype == "post") {
|
|
console.log("Sending POST " + url);
|
|
form_post_request(url);
|
|
} else if (sendtype == "delete") {
|
|
console.log("Sending DELETE " + url);
|
|
form_delete_request(url);
|
|
}
|
|
} else {
|
|
let method = $('#apirequest').val().trim();
|
|
let args = $('#apiargs').val();
|
|
if (args != "") {
|
|
try {
|
|
args = JSON.parse("{" + args + "}");
|
|
} catch (error) {
|
|
console.log("Unable to parse arguments");
|
|
return
|
|
}
|
|
json_rpc.call_method_with_kwargs(method, args)
|
|
.then((result) => {
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(method, error);
|
|
});
|
|
} else {
|
|
json_rpc.call_method(method)
|
|
.then((result) => {
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
update_error(method, error);
|
|
});
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Uploads a selected file to the server
|
|
$('#upload-file').change(() => {
|
|
update_progress(0, 100);
|
|
let file = $('#upload-file').prop('files')[0];
|
|
let dir = get_selected_item("dir");
|
|
if (!dir)
|
|
dir = get_selected_item("root");
|
|
if (file && dir) {
|
|
dir = dir.split('/');
|
|
let root = dir[0];
|
|
let directory = dir.slice(1).join("/");
|
|
console.log("Sending Upload Request...");
|
|
// It might not be a bad idea to validate that this is
|
|
// a gcode file here, and reject and other files.
|
|
|
|
// If you want to allow multiple selections, the below code should be
|
|
// done in a loop, and the 'let file' above should be the entire
|
|
// array of files and not the first element
|
|
|
|
let fdata = new FormData();
|
|
fdata.append("file", file);
|
|
fdata.append("root", root);
|
|
fdata.append("path", directory);
|
|
let settings = {
|
|
url: origin + api.upload.url,
|
|
data: fdata,
|
|
cache: false,
|
|
contentType: false,
|
|
processData: false,
|
|
method: 'POST',
|
|
xhr: () => {
|
|
let xhr = new window.XMLHttpRequest();
|
|
xhr.upload.addEventListener("progress", (evt) => {
|
|
if (evt.lengthComputable) {
|
|
update_progress(evt.loaded, evt.total);
|
|
}
|
|
}, false);
|
|
return xhr;
|
|
},
|
|
success: (resp, status) => {
|
|
console.log(resp);
|
|
return false;
|
|
}
|
|
};
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.ajax(settings);
|
|
$('#upload-file').val('');
|
|
}
|
|
});
|
|
|
|
// Pause/Resume a currently running print. The specific gcode executed
|
|
// is configured in printer.cfg.
|
|
$("#btnpauseresume").click(() =>{
|
|
if (api_type == 'http') {
|
|
let path = paused ? api.resume_print.url : api.pause_print.url
|
|
form_post_request(path);
|
|
} else {
|
|
if (paused) {
|
|
resume_print();
|
|
} else {
|
|
pause_print();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Cancel a currently running print. The specific gcode executed
|
|
// is configured in printer.cfg.
|
|
$("#btncancelprint").click(() =>{
|
|
if (api_type == 'http') {
|
|
form_post_request(api.cancel_print.url);
|
|
} else {
|
|
cancel_print();
|
|
}
|
|
});
|
|
|
|
// Refresh File List
|
|
$("#btngetfiles").click(() =>{
|
|
if (api_type == 'http') {
|
|
form_get_request(api.file_list.url);
|
|
} else {
|
|
get_file_list();
|
|
}
|
|
});
|
|
|
|
$('#btnqueryendstops').click(() => {
|
|
if (api_type == 'http') {
|
|
form_get_request(api.query_endstops.url);
|
|
} else {
|
|
get_endstops();
|
|
}
|
|
});
|
|
|
|
// Post Subscription Request
|
|
$('#btnsubscribe').click(() => {
|
|
if (api_type == 'http') {
|
|
let qs = "?gcode_move=gcode_position,speed,speed_factor,extrude_factor" +
|
|
"&toolhead&virtual_sdcard&heater_bed&extruder=temperature,target&fan&idle_timeout&pause_resume" +
|
|
"&print_stats";
|
|
form_post_request(api.object_subscription.url, qs);
|
|
} else {
|
|
const sub = {
|
|
objects: {
|
|
gcode_move: ["gcode_position", "speed", "speed_factor", "extrude_factor"],
|
|
idle_timeout: null,
|
|
pause_resume: null,
|
|
toolhead: null,
|
|
virtual_sdcard: null,
|
|
heater_bed: null,
|
|
extruder: ["temperature", "target"],
|
|
fan: null,
|
|
print_stats: null}
|
|
};
|
|
add_subscription(sub);
|
|
}
|
|
});
|
|
|
|
$('#btngethelp').click(() => {
|
|
if (api_type == 'http') {
|
|
form_get_request(api.gcode_help.url);
|
|
} else {
|
|
get_gcode_help();
|
|
}
|
|
});
|
|
|
|
$('#btngetobjs').click(() => {
|
|
if (api_type == 'http') {
|
|
form_get_request(api.object_list.url);
|
|
} else {
|
|
get_object_list();
|
|
}
|
|
});
|
|
|
|
$('#btntestmesh').click(() => {
|
|
if (api_type == 'http') {
|
|
let settings = {url: origin + api.object_status.url + "?bed_mesh"};
|
|
if (apikey != null)
|
|
settings.headers = {"X-Api-Key": apikey};
|
|
$.get(settings, (resp, status) => {
|
|
process_mesh(resp.result)
|
|
return false;
|
|
});
|
|
} else {
|
|
get_mesh();
|
|
}
|
|
});
|
|
|
|
$('#btnsendbatch').click(() => {
|
|
let default_gcs = "M118 This works,RESPOND TYPE=invalid,M118 Execs Despite an Error";
|
|
let result = window.prompt("Enter a set of comma separated gcodes:", default_gcs);
|
|
if (result == null || result == "") {
|
|
console.log("Batch GCode Send Cancelled");
|
|
return;
|
|
}
|
|
let gcodes = result.trim().split(',');
|
|
send_gcode_batch(gcodes);
|
|
});
|
|
|
|
$('#btnsendmacro').click(() => {
|
|
let default_gcs = "M118 This works,RESPOND TYPE=invalid,M118 Should Not Exec";
|
|
let result = window.prompt("Enter a set of comma separated gcodes:", default_gcs);
|
|
if (result == null || result == "") {
|
|
console.log("Gcode Macro Cancelled");
|
|
return;
|
|
}
|
|
let gcodes = result.trim().split(',');
|
|
send_gcode_macro(gcodes);
|
|
});
|
|
|
|
$('#btnestop').click(() => {
|
|
if (api_type == 'http') {
|
|
form_post_request(api.estop.url);
|
|
} else {
|
|
emergency_stop();
|
|
}
|
|
});
|
|
|
|
$('#btnrestart').click(() => {
|
|
if (api_type == 'http') {
|
|
form_post_request(api.restart.url);
|
|
} else {
|
|
restart();
|
|
}
|
|
});
|
|
|
|
$('#btnfirmwarerestart').click(() => {
|
|
if (api_type == 'http') {
|
|
form_post_request(api.firmware_restart.url);
|
|
} else {
|
|
firmware_restart();
|
|
}
|
|
});
|
|
|
|
$('#btnreboot').click(() => {
|
|
if (api_type == 'http') {
|
|
form_post_request(api.reboot.url);
|
|
} else {
|
|
reboot();
|
|
}
|
|
});
|
|
|
|
$('#btnshutdown').click(() => {
|
|
if (api_type == 'http') {
|
|
form_post_request(api.shutdown.url);
|
|
} else {
|
|
shutdown();
|
|
}
|
|
});
|
|
|
|
$('#btngetlog').click(() => {
|
|
form_download_request(api.klippy_log.url);
|
|
});
|
|
|
|
$('#btnmoonlog').click(() => {
|
|
form_download_request(api.moonraker_log.url);
|
|
});
|
|
|
|
check_authorization();
|
|
};
|