//~~ View models function ConnectionViewModel() { var self = this; self.portOptions = ko.observableArray(undefined); self.baudrateOptions = ko.observableArray(undefined); self.selectedPort = ko.observable(undefined); self.selectedBaudrate = ko.observable(undefined); self.saveSettings = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self.buttonText = ko.computed(function() { if (self.isErrorOrClosed()) return "Connect"; else return "Disconnect"; }) self.previousIsOperational = undefined; self.requestData = function() { $.ajax({ url: AJAX_BASEURL + "control/connectionOptions", method: "GET", dataType: "json", success: function(response) { self.fromResponse(response); } }) } self.fromResponse = function(response) { self.portOptions(response.ports); self.baudrateOptions(response.baudrates); if (!self.selectedPort() && response.ports && response.ports.indexOf(response.portPreference) >= 0) self.selectedPort(response.portPreference); if (!self.selectedBaudrate() && response.baudrates && response.baudrates.indexOf(response.baudratePreference) >= 0) self.selectedBaudrate(response.baudratePreference); self.saveSettings(false); } self.fromHistoryData = function(data) { self._processStateData(data.state); } self.fromCurrentData = function(data) { self._processStateData(data.state); } self._processStateData = function(data) { self.previousIsOperational = self.isOperational(); self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); var connectionTab = $("#connection"); if (self.previousIsOperational != self.isOperational()) { if (self.isOperational() && connectionTab.hasClass("in")) { // connection just got established, close connection tab for now connectionTab.collapse("hide"); } else if (!connectionTab.hasClass("in")) { // connection just dropped, make sure connection tab is open connectionTab.collapse("show"); } } } self.connect = function() { if (self.isErrorOrClosed()) { var data = { "port": self.selectedPort(), "baudrate": self.selectedBaudrate() }; if (self.saveSettings()) data["save"] = true; $.ajax({ url: AJAX_BASEURL + "control/connect", type: "POST", dataType: "json", data: data }) } else { self.requestData(); $.ajax({ url: AJAX_BASEURL + "control/disconnect", type: "POST", dataType: "json" }) } } } function PrinterStateViewModel() { var self = this; self.stateString = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self.filename = ko.observable(undefined); self.filament = ko.observable(undefined); self.estimatedPrintTime = ko.observable(undefined); self.printTime = ko.observable(undefined); self.printTimeLeft = ko.observable(undefined); self.currentLine = ko.observable(undefined); self.totalLines = ko.observable(undefined); self.currentHeight = ko.observable(undefined); self.lineString = ko.computed(function() { if (!self.totalLines()) return "-"; var currentLine = self.currentLine() ? self.currentLine() : "-"; return currentLine + " / " + self.totalLines(); }); self.progress = ko.computed(function() { if (!self.currentLine() || !self.totalLines()) return 0; return Math.round(self.currentLine() * 100 / self.totalLines()); }); self.pauseString = ko.computed(function() { if (self.isPaused()) return "Continue"; else return "Pause"; }); self.fromCurrentData = function(data) { self._fromData(data); } self.fromHistoryData = function(data) { self._fromData(data); } self._fromData = function(data) { self._processStateData(data.state) self._processJobData(data.job); self._processGcodeData(data.gcode); self._processProgressData(data.progress); self._processZData(data.currentZ); } self._processStateData = function(data) { self.stateString(data.stateString); self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self._processJobData = function(data) { self.filename(data.filename); self.totalLines(data.lines); self.estimatedPrintTime(data.estimatedPrintTime); self.filament(data.filament); } self._processGcodeData = function(data) { if (self.isLoading()) { var progress = Math.round(data.progress * 100); if (data.mode == "loading") { self.filename("Loading... (" + progress + "%)"); } else if (data.mode == "parsing") { self.filename("Parsing... (" + progress + "%)"); } } } self._processProgressData = function(data) { self.currentLine(data.progress); self.printTime(data.printTime); self.printTimeLeft(data.printTimeLeft); } self._processZData = function(data) { self.currentHeight(data); } } function TemperatureViewModel(settingsViewModel) { var self = this; self.temp = ko.observable(undefined); self.bedTemp = ko.observable(undefined); self.targetTemp = ko.observable(undefined); self.bedTargetTemp = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self.temperature_profiles = settingsViewModel.temperature_profiles; self.setTempFromProfile = function(profile) { if (!profile) return; self.setTemp(profile.extruder); } self.setTemp = function(temp) { $.ajax({ url: AJAX_BASEURL + "control/temperature", type: "POST", dataType: "json", data: { temp: temp }, success: function() {$("#temp_newTemp").val("")} }) }; self.setBedTempFromProfile = function(profile) { if (!profile) return; self.setBedTemp(profile.bed); } self.setBedTemp = function(bedTemp) { $.ajax({ url: AJAX_BASEURL + "control/temperature", type: "POST", dataType: "json", data: { bedTemp: bedTemp }, success: function() {$("#temp_newBedTemp").val("")} }) }; self.tempString = ko.computed(function() { if (!self.temp()) return "-"; return self.temp() + " °C"; }); self.bedTempString = ko.computed(function() { if (!self.bedTemp()) return "-"; return self.bedTemp() + " °C"; }); self.targetTempString = ko.computed(function() { if (!self.targetTemp()) return "-"; return self.targetTemp() + " °C"; }); self.bedTargetTempString = ko.computed(function() { if (!self.bedTargetTemp()) return "-"; return self.bedTargetTemp() + " °C"; }); self.temperatures = []; self.plotOptions = { yaxis: { min: 0, max: 310, ticks: 10 }, xaxis: { mode: "time", minTickSize: [2, "minute"], tickFormatter: function(val, axis) { if (val == undefined || val == 0) return ""; // we don't want to display the minutes since the epoch if not connected yet ;) // current time in milliseconds in UTC var timestampUtc = Date.now(); // calculate difference in milliseconds var diff = timestampUtc - val; // convert to minutes var diffInMins = Math.round(diff / (60 * 1000)); if (diffInMins == 0) return "just now"; else return "- " + diffInMins + " min"; } }, legend: { noColumns: 4 } } self.fromCurrentData = function(data) { self._processStateData(data.state); self._processTemperatureUpdateData(data.temperatures); } self.fromHistoryData = function(data) { self._processStateData(data.state); self._processTemperatureHistoryData(data.temperatureHistory); } self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self._processTemperatureUpdateData = function(data) { if (data.length == 0) return; self.temp(data[data.length - 1].temp); self.bedTemp(data[data.length - 1].bedTemp); self.targetTemp(data[data.length - 1].targetTemp); self.bedTargetTemp(data[data.length - 1].targetBedTemp); if (!self.temperatures) self.temperatures = []; if (!self.temperatures.actual) self.temperatures.actual = []; if (!self.temperatures.target) self.temperatures.target = []; if (!self.temperatures.actualBed) self.temperatures.actualBed = []; if (!self.temperatures.targetBed) self.temperatures.targetBed = []; for (var i = 0; i < data.length; i++) { self.temperatures.actual.push([data[i].currentTime, data[i].temp]) self.temperatures.target.push([data[i].currentTime, data[i].targetTemp]) self.temperatures.actualBed.push([data[i].currentTime, data[i].bedTemp]) self.temperatures.targetBed.push([data[i].currentTime, data[i].targetBedTemp]) } self.temperatures.actual = self.temperatures.actual.slice(-300); self.temperatures.target = self.temperatures.target.slice(-300); self.temperatures.actualBed = self.temperatures.actualBed.slice(-300); self.temperatures.targetBed = self.temperatures.targetBed.slice(-300); self.updatePlot(); } self._processTemperatureHistoryData = function(data) { self.temperatures = data; self.updatePlot(); } self.updatePlot = function() { var data = [ {label: "Actual", color: "#FF4040", data: self.temperatures.actual}, {label: "Target", color: "#FFA0A0", data: self.temperatures.target}, {label: "Bed Actual", color: "#4040FF", data: self.temperatures.actualBed}, {label: "Bed Target", color: "#A0A0FF", data: self.temperatures.targetBed} ] $.plot($("#temperature-graph"), data, self.plotOptions); } } function ControlsViewModel() { var self = this; self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self.extrusionAmount = ko.observable(undefined); self.controls = ko.observableArray([]); self.fromCurrentData = function(data) { self._processStateData(data.state); } self.fromHistoryData = function(data) { self._processStateData(data.state); } self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self.requestData = function() { $.ajax({ url: AJAX_BASEURL + "control/custom", method: "GET", dataType: "json", success: function(response) { self._fromResponse(response); } }); } self._fromResponse = function(response) { self.controls(self._enhanceControls(response.controls)); } self._enhanceControls = function(controls) { for (var i = 0; i < controls.length; i++) { controls[i] = self._enhanceControl(controls[i]); } return controls; } self._enhanceControl = function(control) { if (control.type == "parametric_command" || control.type == "parametric_commands") { for (var i = 0; i < control.input.length; i++) { control.input[i].value = control.input[i].default; } } else if (control.type == "section") { control.children = self._enhanceControls(control.children); } return control; } self.sendJogCommand = function(axis, multiplier, distance) { if (typeof distance === "undefined") distance = $('#jog_distance button.active').data('distance'); $.ajax({ url: AJAX_BASEURL + "control/jog", type: "POST", dataType: "json", data: axis + "=" + ( distance * multiplier ) }) } self.sendHomeCommand = function(axis) { $.ajax({ url: AJAX_BASEURL + "control/jog", type: "POST", dataType: "json", data: "home" + axis }) } self.sendExtrudeCommand = function() { self._sendECommand(1); } self.sendRetractCommand = function() { self._sendECommand(-1); } self._sendECommand = function(dir) { var length = self.extrusionAmount(); if (!length) length = 5; $.ajax({ url: AJAX_BASEURL + "control/jog", type: "POST", dataType: "json", data: "extrude=" + (dir * length) }) } self.sendCustomCommand = function(command) { if (!command) return; var data = undefined; if (command.type == "command" || command.type == "parametric_command") { // single command data = {"command" : command.command}; } else if (command.type == "commands" || command.type == "parametric_commands") { // multi command data = {"commands": command.commands}; } if (command.type == "parametric_command" || command.type == "parametric_commands") { // parametric command(s) data["parameters"] = {}; for (var i = 0; i < command.input.length; i++) { data["parameters"][command.input[i].parameter] = command.input[i].value; } } if (!data) return; $.ajax({ url: AJAX_BASEURL + "control/command", type: "POST", dataType: "json", contentType: "application/json; charset=UTF-8", data: JSON.stringify(data) }) } self.displayMode = function(customControl) { switch (customControl.type) { case "section": return "customControls_sectionTemplate"; case "command": case "commands": return "customControls_commandTemplate"; case "parametric_command": case "parametric_commands": return "customControls_parametricCommandTemplate"; default: return "customControls_emptyTemplate"; } } } function SpeedViewModel() { var self = this; self.outerWall = ko.observable(undefined); self.innerWall = ko.observable(undefined); self.fill = ko.observable(undefined); self.support = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self._fromCurrentData = function(data) { self._processStateData(data.state); } self._fromHistoryData = function(data) { self._processStateData(data.state); } self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self.requestData = function() { $.ajax({ url: AJAX_BASEURL + "control/speed", type: "GET", dataType: "json", success: self._fromResponse }); } self._fromResponse = function(response) { if (response.feedrate) { self.outerWall(response.feedrate.outerWall); self.innerWall(response.feedrate.innerWall); self.fill(response.feedrate.fill); self.support(response.feedrate.support); } else { self.outerWall(undefined); self.innerWall(undefined); self.fill(undefined); self.support(undefined); } } } function TerminalViewModel() { var self = this; self.log = []; self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self.autoscrollEnabled = ko.observable(true); self.fromCurrentData = function(data) { self._processStateData(data.state); self._processCurrentLogData(data.logs); } self.fromHistoryData = function(data) { self._processStateData(data.state); self._processHistoryLogData(data.logHistory); } self._processCurrentLogData = function(data) { if (!self.log) self.log = [] self.log = self.log.concat(data) self.updateOutput(); } self._processHistoryLogData = function(data) { self.log = data; self.updateOutput(); } self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self.updateOutput = function() { if (!self.log) return; var output = ""; for (var i = 0; i < self.log.length; i++) { output += self.log[i] + "\n"; } var container = $("#terminal-output"); container.text(output); if (self.autoscrollEnabled()) { container.scrollTop(container[0].scrollHeight - container.height()) } } } function GcodeFilesViewModel() { var self = this; self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); // initialize list helper self.listHelper = new ItemListHelper( "gcodeFiles", { "name": function(a, b) { // sorts ascending if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; return 0; }, "upload": function(a, b) { // sorts descending if (a["date"] > b["date"]) return -1; if (a["date"] < b["date"]) return 1; return 0; }, "size": function(a, b) { // sorts descending if (a["bytes"] > b["bytes"]) return -1; if (a["bytes"] < b["bytes"]) return 1; return 0; } }, { "printed": function(file) { return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0); } }, "name", [], CONFIG_GCODEFILESPERPAGE ); self.isLoadActionPossible = ko.computed(function() { return !self.isPrinting() && !self.isPaused() && !self.isLoading(); }); self.isLoadAndPrintActionPossible = ko.computed(function() { return self.isOperational() && self.isLoadActionPossible(); }); self.fromCurrentData = function(data) { self._processStateData(data.state); } self.fromHistoryData = function(data) { self._processStateData(data.state); } self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self.requestData = function() { $.ajax({ url: AJAX_BASEURL + "gcodefiles", method: "GET", dataType: "json", success: function(response) { self.fromResponse(response); } }); } self.fromResponse = function(response) { self.listHelper.updateItems(response.files); if (response.filename) { // got a file to scroll to self.listHelper.switchToItem(function(item) {return item.name == response.filename}); } } self.loadFile = function(filename, printAfterLoad) { $.ajax({ url: AJAX_BASEURL + "gcodefiles/load", type: "POST", dataType: "json", data: {filename: filename, print: printAfterLoad} }) } self.removeFile = function(filename) { $.ajax({ url: AJAX_BASEURL + "gcodefiles/delete", type: "POST", dataType: "json", data: {filename: filename}, success: self.fromResponse }) } self.getPopoverContent = function(data) { var output = "

Uploaded: " + data["date"] + "

"; if (data["gcodeAnalysis"]) { output += "

"; output += "Filament: " + data["gcodeAnalysis"]["filament"] + "
"; output += "Estimated Print Time: " + data["gcodeAnalysis"]["estimatedPrintTime"]; output += "

"; } if (data["prints"] && data["prints"]["last"]) { output += "

"; output += "Last Print: " + data["prints"]["last"]["date"] + ""; output += "

"; } return output; } self.getSuccessClass = function(data) { if (!data["prints"] || !data["prints"]["last"]) { return ""; } return data["prints"]["last"]["success"] ? "text-success" : "text-error"; } } function WebcamViewModel() { var self = this; self.timelapseType = ko.observable(undefined); self.timelapseTimedInterval = ko.observable(undefined); self.isErrorOrClosed = ko.observable(undefined); self.isOperational = ko.observable(undefined); self.isPrinting = ko.observable(undefined); self.isPaused = ko.observable(undefined); self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); self.intervalInputEnabled = ko.computed(function() { return ("timed" == self.timelapseType()); }) self.isOperational.subscribe(function(newValue) { self.requestData(); }) // initialize list helper self.listHelper = new ItemListHelper( "timelapseFiles", { "name": function(a, b) { // sorts ascending if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; return 0; }, "creation": function(a, b) { // sorts descending if (a["date"] > b["date"]) return -1; if (a["date"] < b["date"]) return 1; return 0; }, "size": function(a, b) { // sorts descending if (a["bytes"] > b["bytes"]) return -1; if (a["bytes"] < b["bytes"]) return 1; return 0; } }, { }, "name", [], CONFIG_TIMELAPSEFILESPERPAGE ) self.requestData = function() { $.ajax({ url: AJAX_BASEURL + "timelapse", type: "GET", dataType: "json", success: self.fromResponse }); $("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime()); } self.fromResponse = function(response) { self.timelapseType(response.type); self.listHelper.updateItems(response.files); if (response.type == "timed" && response.config && response.config.interval) { self.timelapseTimedInterval(response.config.interval) } else { self.timelapseTimedInterval(undefined) } } self.fromCurrentData = function(data) { self._processStateData(data.state); } self.fromHistoryData = function(data) { self._processStateData(data.state); } self._processStateData = function(data) { self.isErrorOrClosed(data.flags.closedOrError); self.isOperational(data.flags.operational); self.isPaused(data.flags.paused); self.isPrinting(data.flags.printing); self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); } self.removeFile = function() { var filename = this.name; $.ajax({ url: AJAX_BASEURL + "timelapse/" + filename, type: "DELETE", dataType: "json", success: self.requestData }) } self.save = function() { var data = { "type": self.timelapseType() } if (self.timelapseType() == "timed") { data["interval"] = self.timelapseTimedInterval(); } $.ajax({ url: AJAX_BASEURL + "timelapse/config", type: "POST", dataType: "json", data: data, success: self.fromResponse }) } } function GcodeViewModel() { var self = this; self.loadedFilename = undefined; self.status = 'idle'; self.enabled = false; self.initialize = function(){ self.enabled = true; GCODE.ui.initHandlers(); } self.loadFile = function(filename){ if (self.status == 'idle') { self.status = 'request'; $.ajax({ url: AJAX_BASEURL + "gcodefiles/" + filename, type: "GET", success: function(response, rstatus) { if(rstatus === 'success'){ self.showGCodeViewer(response, rstatus); self.loadedFilename=filename; self.status = 'idle'; } }, error: function() { self.status = 'idle'; } }) } } self.showGCodeViewer = function(response, rstatus) { var par = {}; par.target = {}; par.target.result = response; GCODE.gCodeReader.loadFile(par); } self.fromHistoryData = function(data) { self._processData(data); } self.fromCurrentData = function(data) { self._processData(data); } self._processData = function(data) { if(!self.enabled)return; if(self.loadedFilename == data.job.filename) { var cmdIndex = GCODE.gCodeReader.getLinesCmdIndex(data.progress.progress); if(cmdIndex){ GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); GCODE.ui.updateLayerInfo(cmdIndex.layer); } } else if (data.job.filename) { self.loadFile(data.job.filename); } } } function SettingsViewModel() { var self = this; self.appearance_name = ko.observable(undefined); self.appearance_color = ko.observable(undefined); /* I did attempt to allow arbitrary gradients but cross browser support via knockout or jquery was going to be horrible */ self.appearance_available_colors = ko.observable(["default", "red", "orange", "yellow", "green", "blue", "violet"]); self.printer_movementSpeedX = ko.observable(undefined); self.printer_movementSpeedY = ko.observable(undefined); self.printer_movementSpeedZ = ko.observable(undefined); self.printer_movementSpeedE = ko.observable(undefined); self.webcam_streamUrl = ko.observable(undefined); self.webcam_snapshotUrl = ko.observable(undefined); self.webcam_ffmpegPath = ko.observable(undefined); self.webcam_bitrate = ko.observable(undefined); self.webcam_watermark = ko.observable(undefined); self.feature_gcodeViewer = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined); self.folder_uploads = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined); self.folder_timelapseTmp = ko.observable(undefined); self.folder_logs = ko.observable(undefined); self.temperature_profiles = ko.observableArray(undefined); self.system_actions = ko.observableArray([]); self.addTemperatureProfile = function() { self.temperature_profiles.push({name: "New", extruder:0, bed:0}); }; self.removeTemperatureProfile = function(profile) { self.temperature_profiles.remove(profile); }; self.requestData = function() { $.ajax({ url: AJAX_BASEURL + "settings", type: "GET", dataType: "json", success: self.fromResponse }) } self.fromResponse = function(response) { self.appearance_name(response.appearance.name); self.appearance_color(response.appearance.color); self.printer_movementSpeedX(response.printer.movementSpeedX); self.printer_movementSpeedY(response.printer.movementSpeedY); self.printer_movementSpeedZ(response.printer.movementSpeedZ); self.printer_movementSpeedE(response.printer.movementSpeedE); self.webcam_streamUrl(response.webcam.streamUrl); self.webcam_snapshotUrl(response.webcam.snapshotUrl); self.webcam_ffmpegPath(response.webcam.ffmpegPath); self.webcam_bitrate(response.webcam.bitrate); self.webcam_watermark(response.webcam.watermark); self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_waitForStart(response.feature.waitForStart); self.folder_uploads(response.folder.uploads); self.folder_timelapse(response.folder.timelapse); self.folder_timelapseTmp(response.folder.timelapseTmp); self.folder_logs(response.folder.logs); self.temperature_profiles(response.temperature.profiles); self.system_actions(response.system.actions); } self.saveData = function() { var data = { "appearance" : { "name": self.appearance_name(), "color": self.appearance_color() }, "printer": { "movementSpeedX": self.printer_movementSpeedX(), "movementSpeedY": self.printer_movementSpeedY(), "movementSpeedZ": self.printer_movementSpeedZ(), "movementSpeedE": self.printer_movementSpeedE() }, "webcam": { "streamUrl": self.webcam_streamUrl(), "snapshotUrl": self.webcam_snapshotUrl(), "ffmpegPath": self.webcam_ffmpegPath(), "bitrate": self.webcam_bitrate(), "watermark": self.webcam_watermark() }, "feature": { "gcodeViewer": self.feature_gcodeViewer(), "waitForStart": self.feature_waitForStart() }, "folder": { "uploads": self.folder_uploads(), "timelapse": self.folder_timelapse(), "timelapseTmp": self.folder_timelapseTmp(), "logs": self.folder_logs() }, "temperature": { "profiles": self.temperature_profiles() }, "system": { "actions": self.system_actions() } } $.ajax({ url: AJAX_BASEURL + "settings", type: "POST", dataType: "json", contentType: "application/json; charset=UTF-8", data: JSON.stringify(data), success: function(response) { self.fromResponse(response); $("#settings_dialog").modal("hide"); } }) } } function NavigationViewModel(appearanceViewModel, settingsViewModel) { var self = this; self.appearance = appearanceViewModel; self.systemActions = settingsViewModel.system_actions; self.triggerAction = function(action) { var callback = function() { $.ajax({ url: AJAX_BASEURL + "system", type: "POST", dataType: "json", data: "action=" + action.action, success: function() { $.pnotify({title: "Success", text: "The command \""+ action.name +"\" executed successfully", type: "success"}); }, error: function(jqXHR, textStatus, errorThrown) { $.pnotify({title: "Error", text: "

The command \"" + action.name + "\" could not be executed.

Reason:

" + jqXHR.responseText + "

", type: "error"}); } }) } if (action.confirm) { $("#confirmation_dialog .confirmation_dialog_message").text(action.confirm); $("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); callback(); }); $("#confirmation_dialog").modal("show"); } else { callback(); } } } function DataUpdater(connectionViewModel, printerStateViewModel, temperatureViewModel, controlsViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, webcamViewModel, gcodeViewModel) { var self = this; self.connectionViewModel = connectionViewModel; self.printerStateViewModel = printerStateViewModel; self.temperatureViewModel = temperatureViewModel; self.controlsViewModel = controlsViewModel; self.terminalViewModel = terminalViewModel; self.speedViewModel = speedViewModel; self.gcodeFilesViewModel = gcodeFilesViewModel; self.webcamViewModel = webcamViewModel; self.gcodeViewModel = gcodeViewModel; self._socket = io.connect(); self._socket.on("connect", function() { if ($("#offline_overlay").is(":visible")) { $("#offline_overlay").hide(); self.webcamViewModel.requestData(); } }) self._socket.on("disconnect", function() { $("#offline_overlay_message").html( "The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect " + "automatically over the next couple of minutes, however you are welcome to try a manual reconnect " + "anytime using the button below." ); if (!$("#offline_overlay").is(":visible")) $("#offline_overlay").show(); }) self._socket.on("reconnect_failed", function() { $("#offline_overlay_message").html( "The server appears to be offline, at least I'm not getting any response from it. I could not reconnect automatically, " + "but you may try a manual reconnect using the button below." ); }) self._socket.on("history", function(data) { self.connectionViewModel.fromHistoryData(data); self.printerStateViewModel.fromHistoryData(data); self.temperatureViewModel.fromHistoryData(data); self.controlsViewModel.fromHistoryData(data); self.terminalViewModel.fromHistoryData(data); self.webcamViewModel.fromHistoryData(data); self.gcodeViewModel.fromHistoryData(data); self.gcodeFilesViewModel.fromCurrentData(data); }) self._socket.on("current", function(data) { self.connectionViewModel.fromCurrentData(data); self.printerStateViewModel.fromCurrentData(data); self.temperatureViewModel.fromCurrentData(data); self.controlsViewModel.fromCurrentData(data); self.terminalViewModel.fromCurrentData(data); self.webcamViewModel.fromCurrentData(data); self.gcodeViewModel.fromCurrentData(data); self.gcodeFilesViewModel.fromCurrentData(data); }) self._socket.on("updateTrigger", function(type) { if (type == "gcodeFiles") { gcodeFilesViewModel.requestData(); } }) self.reconnect = function() { self._socket.socket.connect(); } } function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, filesPerPage) { var self = this; self.listType = listType; self.supportedSorting = supportedSorting; self.supportedFilters = supportedFilters; self.defaultSorting = defaultSorting; self.defaultFilters = defaultFilters; self.allItems = []; self.items = ko.observableArray([]); self.pageSize = ko.observable(filesPerPage); self.currentPage = ko.observable(0); self.currentSorting = ko.observable(self.defaultSorting); self.currentFilters = ko.observableArray(self.defaultFilters); //~~ item handling self.updateItems = function(items) { self.allItems = items; self._updateItems(); } //~~ pagination self.paginatedItems = ko.dependentObservable(function() { if (self.items() == undefined) { return []; } else { var from = Math.max(self.currentPage() * self.pageSize(), 0); var to = Math.min(from + self.pageSize(), self.items().length); return self.items().slice(from, to); } }) self.lastPage = ko.dependentObservable(function() { return Math.ceil(self.items().length / self.pageSize()) - 1; }) self.pages = ko.dependentObservable(function() { var pages = []; if (self.lastPage() < 7) { for (var i = 0; i < self.lastPage() + 1; i++) { pages.push({ number: i, text: i+1 }); } } else { pages.push({ number: 0, text: 1 }); if (self.currentPage() < 5) { for (var i = 1; i < 5; i++) { pages.push({ number: i, text: i+1 }); } pages.push({ number: -1, text: "…"}); } else if (self.currentPage() > self.lastPage() - 5) { pages.push({ number: -1, text: "…"}); for (var i = self.lastPage() - 4; i < self.lastPage(); i++) { pages.push({ number: i, text: i+1 }); } } else { pages.push({ number: -1, text: "…"}); for (var i = self.currentPage() - 1; i <= self.currentPage() + 1; i++) { pages.push({ number: i, text: i+1 }); } pages.push({ number: -1, text: "…"}); } pages.push({ number: self.lastPage(), text: self.lastPage() + 1}) } return pages; }) self.switchToItem = function(matcher) { var pos = -1; var itemList = self.items(); for (var i = 0; i < itemList.length; i++) { if (matcher(itemList[i])) { pos = i; break; } } if (pos > -1) { var page = Math.floor(pos / self.pageSize()); self.changePage(page); } } self.changePage = function(newPage) { if (newPage < 0 || newPage > self.lastPage()) return; self.currentPage(newPage); } self.prevPage = function() { if (self.currentPage() > 0) { self.currentPage(self.currentPage() - 1); } } self.nextPage = function() { if (self.currentPage() < self.lastPage()) { self.currentPage(self.currentPage() + 1); } } //~~ sorting self.changeSorting = function(sorting) { if (!_.contains(_.keys(self.supportedSorting), sorting)) return; self.currentSorting(sorting); self._saveCurrentSortingToLocalStorage(); self.changePage(0); self._updateItems(); } //~~ filtering self.toggleFilter = function(filter) { if (!_.contains(_.keys(self.supportedFilters), filter)) return; if (_.contains(self.currentFilters(), filter)) { self.removeFilter(filter); } else { self.addFilter(filter); } } self.addFilter = function(filter) { if (!_.contains(_.keys(self.supportedFilters), filter)) return; var filters = self.currentFilters(); filters.push(filter); self.currentFilters(filters); self._saveCurrentFiltersToLocalStorage(); self._updateItems(); } self.removeFilter = function(filter) { if (filter != "printed") return; var filters = self.currentFilters(); filters.pop(filter); self.currentFilters(filters); self._saveCurrentFiltersToLocalStorage(); self._updateItems(); } //~~ update for sorted and filtered view self._updateItems = function() { // determine comparator var comparator = undefined; var currentSorting = self.currentSorting(); if (typeof currentSorting !== undefined && typeof self.supportedSorting[currentSorting] !== undefined) { comparator = self.supportedSorting[currentSorting]; } // work on all items var result = self.allItems; // filter if necessary var filters = self.currentFilters(); _.each(filters, function(filter) { if (typeof filter !== undefined && typeof supportedFilters[filter] !== undefined) result = _.filter(result, supportedFilters[filter]); }); // sort if necessary if (typeof comparator !== undefined) result.sort(comparator); // set result list self.items(result); } //~~ local storage self._saveCurrentSortingToLocalStorage = function() { self._initializeLocalStorage(); var currentSorting = self.currentSorting(); if (currentSorting !== undefined) localStorage[self.listType + "." + "currentSorting"] = currentSorting; else localStorage[self.listType + "." + "currentSorting"] = undefined; } self._loadCurrentSortingFromLocalStorage = function() { self._initializeLocalStorage(); if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"])) self.currentSorting(localStorage[self.listType + "." + "currentSorting"]); else self.currentSorting(defaultSorting); } self._saveCurrentFiltersToLocalStorage = function() { self._initializeLocalStorage(); var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters()); localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters); } self._loadCurrentFiltersFromLocalStorage = function() { self._initializeLocalStorage(); self.currentFilters(_.intersection(_.keys(self.supportedFilters), JSON.parse(localStorage[self.listType + "." + "currentFilters"]))); } self._initializeLocalStorage = function() { if (localStorage[self.listType + "." + "currentSorting"] !== undefined && localStorage[self.listType + "." + "currentFilters"] !== undefined && JSON.parse(localStorage[self.listType + "." + "currentFilters"]) instanceof Array) return; localStorage[self.listType + "." + "currentSorting"] = self.defaultSorting; localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(self.defaultFilters); } self._loadCurrentFiltersFromLocalStorage(); self._loadCurrentSortingFromLocalStorage(); } function AppearanceViewModel(settingsViewModel) { var self = this; self.name = settingsViewModel.appearance_name; self.color = settingsViewModel.appearance_color; self.brand = ko.computed(function() { if (self.name()) return "OctoPrint: " + self.name(); else return "OctoPrint"; }) self.title = ko.computed(function() { if (self.name()) return self.name() + " [OctoPrint]"; else return "OctoPrint"; }) } $(function() { //~~ View models var connectionViewModel = new ConnectionViewModel(); var printerStateViewModel = new PrinterStateViewModel(); var settingsViewModel = new SettingsViewModel(); var appearanceViewModel = new AppearanceViewModel(settingsViewModel); var temperatureViewModel = new TemperatureViewModel(settingsViewModel); var controlsViewModel = new ControlsViewModel(); var speedViewModel = new SpeedViewModel(); var terminalViewModel = new TerminalViewModel(); var gcodeFilesViewModel = new GcodeFilesViewModel(); var webcamViewModel = new WebcamViewModel(); var gcodeViewModel = new GcodeViewModel(); var navigationViewModel = new NavigationViewModel(appearanceViewModel, settingsViewModel); var dataUpdater = new DataUpdater( connectionViewModel, printerStateViewModel, temperatureViewModel, controlsViewModel, speedViewModel, terminalViewModel, gcodeFilesViewModel, webcamViewModel, gcodeViewModel ); //work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results $.ajaxSetup({ type: 'POST', headers: { "cache-control": "no-cache" } }); //~~ Show settings - to ensure centered $('#navbar_show_settings').click(function() { $('#settings_dialog').modal() .css({ width: 'auto', 'margin-left': function() { return -($(this).width() /2); } }); return false; }) //~~ Print job control $("#job_print").click(function() { $.ajax({ url: AJAX_BASEURL + "control/print", type: "POST", dataType: "json", success: function(){} }) }) $("#job_pause").click(function() { $("#job_pause").button("toggle"); $.ajax({ url: AJAX_BASEURL + "control/pause", type: "POST", dataType: "json" }) }) $("#job_cancel").click(function() { $.ajax({ url: AJAX_BASEURL + "control/cancel", type: "POST", dataType: "json" }) }) //~~ Temperature control (should really move to knockout click binding) $("#temp_newTemp_set").click(function() { var newTemp = $("#temp_newTemp").val(); $.ajax({ url: AJAX_BASEURL + "control/temperature", type: "POST", dataType: "json", data: { temp: newTemp }, success: function() {$("#temp_newTemp").val("")} }) }) $("#temp_newBedTemp_set").click(function() { var newBedTemp = $("#temp_newBedTemp").val(); $.ajax({ url: AJAX_BASEURL + "control/temperature", type: "POST", dataType: "json", data: { bedTemp: newBedTemp }, success: function() {$("#temp_newBedTemp").val("")} }) }) $('#tabs a[data-toggle="tab"]').on('shown', function (e) { temperatureViewModel.updatePlot(); }); //~~ Speed controls function speedCommand(structure) { var speedSetting = $("#speed_" + structure).val(); if (speedSetting) { $.ajax({ url: AJAX_BASEURL + "control/speed", type: "POST", dataType: "json", data: structure + "=" + speedSetting, success: function(response) { $("#speed_" + structure).val("") speedViewModel.fromResponse(response); } }) } } $("#speed_outerWall_set").click(function() {speedCommand("outerWall")}); $("#speed_innerWall_set").click(function() {speedCommand("innerWall")}); $("#speed_support_set").click(function() {speedCommand("support")}); $("#speed_fill_set").click(function() {speedCommand("fill")}); //~~ Terminal $("#terminal-send").click(function () { var command = $("#terminal-command").val(); if (command) { $.ajax({ url: AJAX_BASEURL + "control/command", type: "POST", dataType: "json", contentType: "application/json; charset=UTF-8", data: JSON.stringify({"command": command}) }) $("#terminal-command").val('') } }) $("#terminal-command").keyup(function (event) { if (event.keyCode == 13) { $("#terminal-send").click() } }) //~~ Gcode upload $("#gcode_upload").fileupload({ dataType: "json", done: function (e, data) { gcodeFilesViewModel.fromResponse(data.result); $("#gcode_upload_progress .bar").css("width", "0%"); $("#gcode_upload_progress").removeClass("progress-striped").removeClass("active"); $("#gcode_upload_progress .bar").text(""); }, progressall: function (e, data) { var progress = parseInt(data.loaded / data.total * 100, 10); $("#gcode_upload_progress .bar").css("width", progress + "%"); $("#gcode_upload_progress .bar").text("Uploading ..."); if (progress >= 100) { $("#gcode_upload_progress").addClass("progress-striped").addClass("active"); $("#gcode_upload_progress .bar").text("Saving ..."); } } }); //~~ Offline overlay $("#offline_overlay_reconnect").click(function() {dataUpdater.reconnect()}); //~~ Alert /* function displayAlert(text, timeout, type) { var placeholder = $("#alert_placeholder"); var alertType = ""; if (type == "success" || type == "error" || type == "info") { alertType = " alert-" + type; } placeholder.append($("

" + text + "

")); placeholder.fadeIn(); $("#activeAlert").delay(timeout).fadeOut("slow", function() {$(this).remove(); $("#alert_placeholder").hide();}); } */ //~~ knockout.js bindings ko.bindingHandlers.popover = { init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var val = ko.utils.unwrapObservable(valueAccessor()); var options = { title: val.title, animation: val.animation, placement: val.placement, trigger: val.trigger, delay: val.delay, content: val.content, html: val.html }; $(element).popover(options); } } ko.applyBindings(connectionViewModel, document.getElementById("connection")); ko.applyBindings(printerStateViewModel, document.getElementById("state")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files")); ko.applyBindings(gcodeFilesViewModel, document.getElementById("files-heading")); ko.applyBindings(temperatureViewModel, document.getElementById("temp")); ko.applyBindings(controlsViewModel, document.getElementById("controls")); ko.applyBindings(terminalViewModel, document.getElementById("term")); ko.applyBindings(speedViewModel, document.getElementById("speed")); ko.applyBindings(gcodeViewModel, document.getElementById("gcode")); ko.applyBindings(settingsViewModel, document.getElementById("settings_dialog")); ko.applyBindings(navigationViewModel, document.getElementById("navbar")); ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]); var webcamElement = document.getElementById("webcam"); if (webcamElement) { ko.applyBindings(webcamViewModel, document.getElementById("webcam")); } var gCodeVisualizerElement = document.getElementById("gcode"); if(gCodeVisualizerElement){ gcodeViewModel.initialize(); } //~~ startup commands connectionViewModel.requestData(); controlsViewModel.requestData(); gcodeFilesViewModel.requestData(); webcamViewModel.requestData(); settingsViewModel.requestData(); //~~ UI stuff $(".accordion-toggle[href='#files']").click(function() { if ($("#files").hasClass("in")) { $("#files").removeClass("overflow_visible"); } else { setTimeout(function() { $("#files").addClass("overflow_visible"); }, 1000); } }) $.pnotify.defaults.history = false; } );