diff --git a/octoprint/printer.py b/octoprint/printer.py index cc0e41e..85ccdc8 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -66,6 +66,11 @@ class Printer(): self._filename = None self._gcodeLoader = None + # sd handling + self._sdPrinting = False + self._sdFile = None + self._sdStreamer = None + # feedrate self._feedrateModifierMapping = {"outerWall": "WALL-OUTER", "innerWall": "WALL_INNER", "fill": "FILL", "support": "SUPPORT"} @@ -90,6 +95,7 @@ class Printer(): state={"state": None, "stateString": self.getStateString(), "flags": self._getStateFlags()}, jobData={"filename": None, "lines": None, "estimatedPrintTime": None, "filament": None}, gcodeData={"filename": None, "progress": None}, + sdUploadData={"filename": None, "progress": None}, progress={"progress": None, "printTime": None, "printTimeLeft": None}, currentZ=None ) @@ -124,6 +130,11 @@ class Printer(): try: callback.sendCurrentData(copy.deepcopy(data)) except: pass + def _sendTriggerUpdateCallbacks(self, type): + for callback in self._callbacks: + try: callback.sendUpdateTrigger(type) + except: pass + #~~ printer commands def connect(self, port=None, baudrate=None): @@ -171,6 +182,7 @@ class Printer(): if (self._comm is not None and self._comm.isPrinting()) or (self._gcodeLoader is not None): return + self._sdFile = None self._setJobData(None, None) onGcodeLoadedCallback = self._onGcodeLoaded @@ -189,14 +201,19 @@ class Printer(): """ if self._comm is None or not self._comm.isOperational(): return - if self._gcodeList is None: + if self._gcodeList is None and self._sdFile is None: return if self._comm.isPrinting(): return self._setCurrentZ(-1) - - self._comm.printGCode(self._gcodeList) + if self._sdFile is not None: + # we are working in sd mode + self._sdPrinting = True + self._comm.printSdFile() + else: + # we are working in local mode + self._comm.printGCode(self._gcodeList) def togglePausePrint(self): """ @@ -212,16 +229,20 @@ class Printer(): """ if self._comm is None: return + + if self._sdPrinting: + self._sdPrinting = False self._comm.cancelPrint() + if disableMotorsAndHeater: self.commands(["M84", "M104 S0", "M140 S0", "M106 S0"]) # disable motors, switch off heaters and fan # reset line, height, print time self._setCurrentZ(None) - self._setProgressData(None, None, None) + self._setProgressData(None, None, None, None) # mark print as failure - if self._filename: + if self._filename is not None: self._gcodeManager.printFailed(self._filename) eventManager().fire("PrintFailed", self._filename) @@ -258,7 +279,7 @@ class Printer(): self._messages = self._messages[-300:] self._stateMonitor.addMessage(message) - def _setProgressData(self, progress, printTime, printTimeLeft): + def _setProgressData(self, progress, currentLine, printTime, printTimeLeft): self._progress = progress self._printTime = printTime self._printTimeLeft = printTimeLeft @@ -271,7 +292,7 @@ class Printer(): if (self._printTimeLeft): formattedPrintTimeLeft = util.getFormattedTimeDelta(datetime.timedelta(minutes=self._printTimeLeft)) - self._stateMonitor.setProgress({"progress": self._progress, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) + self._stateMonitor.setProgress({"progress": self._progress, "currentLine": currentLine, "printTime": formattedPrintTime, "printTimeLeft": formattedPrintTimeLeft}) def _addTemperatureData(self, temp, bedTemp, targetTemp, bedTargetTemp): currentTimeUtc = int(time.time() * 1000) @@ -333,6 +354,11 @@ class Printer(): pass def _getStateFlags(self): + if not settings().getBoolean(["feature", "sdSupport"]) or self._comm is None: + sdReady = False + else: + sdReady = self._comm.isSdReady() + return { "operational": self.isOperational(), "printing": self.isPrinting(), @@ -340,7 +366,8 @@ class Printer(): "error": self.isError(), "loading": self.isLoading(), "paused": self.isPaused(), - "ready": self.isReady() + "ready": self.isReady(), + "sdReady": sdReady } def getCurrentData(self): @@ -376,13 +403,12 @@ class Printer(): self._gcodeManager.printSucceeded(self._filename) elif state == self._comm.STATE_CLOSED or state == self._comm.STATE_ERROR or state == self._comm.STATE_CLOSED_WITH_ERROR: self._gcodeManager.printFailed(self._filename) - self._gcodeManager.resumeAnalysis() # do not analyse gcode while printing + self._gcodeManager.resumeAnalysis() # printing done, put those cpu cycles to good use elif self._comm is not None and state == self._comm.STATE_PRINTING: - self._gcodeManager.pauseAnalysis() # printing done, put those cpu cycles to good use + self._gcodeManager.pauseAnalysis() # do not analyse gcode while printing self._setState(state) - def mcMessage(self, message): """ Callback method for the comm object, called upon message exchanges via serial. @@ -390,18 +416,28 @@ class Printer(): """ self._addMessage(message) - def mcProgress(self, lineNr): + def mcProgress(self): """ Callback method for the comm object, called upon any change in progress of the printjob. Triggers storage of new values for printTime, printTimeLeft and the current line. """ oldProgress = self._progress - if self._timelapse is not None: - try: self._timelapse.onPrintjobProgress(oldProgress, self._progress, int(round(self._progress * 100 / len(self._gcodeList)))) - except: pass + if self._sdPrinting: + newLine = None + (filePos, fileSize) = self._comm.getSdProgress() + if fileSize > 0: + newProgress = float(filePos) / float(fileSize) + else: + newProgress = 0.0 + else: + newLine = self._comm.getPrintPos() + if self._gcodeList is not None: + newProgress = float(newLine) / float(len(self._gcodeList)) + else: + newProgress = 0.0 - self._setProgressData(self._comm.getPrintPos(), self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) + self._setProgressData(newProgress, newLine, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) def mcZChange(self, newZ): """ @@ -418,6 +454,83 @@ class Printer(): self._setCurrentZ(newZ) + def mcSdStateChange(self, sdReady): + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + def mcSdFiles(self, files): + self._sendTriggerUpdateCallbacks("gcodeFiles") + + def mcSdSelected(self, filename, filesize): + self._sdFile = filename + + self._setJobData(filename, None) + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + if self._sdPrintAfterSelect: + self.startPrint() + + def mcSdPrintingDone(self): + self._sdPrinting = False + self._setProgressData(1.0, None, self._comm.getPrintTime(), self._comm.getPrintTimeRemainingEstimate()) + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + + #~~ sd file handling + + def getSdFiles(self): + if self._comm is None: + return + return self._comm.getSdFiles() + + def addSdFile(self, filename, file): + if not self._comm: + return + + self._sdStreamer = SdFileStreamer(self._comm, filename, file, self._onSdFileStreamProgress, self._onSdFileStreamFinish) + self._sdStreamer.start() + + def deleteSdFile(self, filename): + if not self._comm: + return + + if self._sdFile == filename: + self._sdFile = None + self._comm.deleteSdFile(filename) + + def selectSdFile(self, filename, printAfterSelect): + if not self._comm: + return + + self._sdPrintAfterSelect = printAfterSelect + self._comm.selectSdFile(filename) + + def initSdCard(self): + if not self._comm: + return + self._comm.initSdCard() + + def releaseSdCard(self): + if not self._comm: + return + self._comm.releaseSdCard() + + def refreshSdFiles(self): + if not self._comm: + return + self._comm.refreshSdFiles() + + #~~ callbacks triggered by sdFileStreamer + + def _onSdFileStreamProgress(self, filename, progress): + self._stateMonitor.setSdUploadData({"filename": filename, "progress": progress}) + + def _onSdFileStreamFinish(self, filename): + self._setCurrentZ(None) + self._setProgressData(None, None, None, None) + self._sdStreamer = None + + self._stateMonitor.setSdUploadData({"filename": None, "progress": None}) + self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) + #~~ callbacks triggered by gcodeLoader def _onGcodeLoadingProgress(self, filename, progress, mode): @@ -430,7 +543,7 @@ class Printer(): def _onGcodeLoaded(self, filename, gcodeList): self._setJobData(filename, gcodeList) self._setCurrentZ(None) - self._setProgressData(None, None, None) + self._setProgressData(None, None, None, None) self._gcodeLoader = None self._stateMonitor.setGcodeData({"filename": None, "progress": None}) self._stateMonitor.setState({"state": self._state, "stateString": self.getStateString(), "flags": self._getStateFlags()}) @@ -481,10 +594,10 @@ class Printer(): return self._comm is not None and self._comm.isError() def isReady(self): - return self._gcodeLoader is None and self._gcodeList and len(self._gcodeList) > 0 + return self._gcodeLoader is None and self._sdStreamer is None and ((self._gcodeList and len(self._gcodeList) > 0) or self._sdFile) def isLoading(self): - return self._gcodeLoader is not None + return self._gcodeLoader is not None or self._sdStreamer is not None @@ -507,7 +620,7 @@ class GcodeLoader(threading.Thread): def run(self): #Send an initial M110 to reset the line counter to zero. prevLineType = lineType = "CUSTOM" - gcodeList = ["M110"] + gcodeList = ["M110 N0"] filesize = os.stat(self._filename).st_size with open(self._filename, "r") as file: for line in file: @@ -533,6 +646,38 @@ class GcodeLoader(threading.Thread): def _onParsingProgress(self, progress): self._progressCallback(self._filename, progress, "parsing") +class SdFileStreamer(threading.Thread): + def __init__(self, comm, filename, file, progressCallback, finishCallback): + threading.Thread.__init__(self) + + self._comm = comm + self._filename = filename + self._file = file + self._progressCallback = progressCallback + self._finishCallback = finishCallback + + def run(self): + if self._comm.isBusy(): + return + + name = self._filename[:self._filename.rfind(".")] + sdFilename = name[:8] + ".GCO" + try: + size = os.stat(self._file).st_size + with open(self._file, "r") as f: + self._comm.startSdFileTransfer(sdFilename) + for line in f: + if ";" in line: + line = line[0:line.find(";")] + line = line.strip() + if len(line) > 0: + self._comm.sendCommand(line) + time.sleep(0.001) # do not send too fast + self._progressCallback(sdFilename, float(f.tell()) / float(size)) + finally: + self._comm.endSdFileTransfer(sdFilename) + self._finishCallback(sdFilename) + class StateMonitor(object): def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): self._ratelimit = ratelimit @@ -544,6 +689,7 @@ class StateMonitor(object): self._state = None self._jobData = None self._gcodeData = None + self._sdUploadData = None self._currentZ = None self._peakZ = -1 self._progress = None @@ -555,10 +701,11 @@ class StateMonitor(object): self._worker.daemon = True self._worker.start() - def reset(self, state=None, jobData=None, gcodeData=None, progress=None, currentZ=None): + def reset(self, state=None, jobData=None, gcodeData=None, sdUploadData=None, progress=None, currentZ=None): self.setState(state) self.setJobData(jobData) self.setGcodeData(gcodeData) + self.setSdUploadData(sdUploadData) self.setProgress(progress) self.setCurrentZ(currentZ) @@ -590,6 +737,10 @@ class StateMonitor(object): self._gcodeData = gcodeData self._changeEvent.set() + def setSdUploadData(self, uploadData): + self._sdUploadData = uploadData + self._changeEvent.set() + def setProgress(self, progress): self._progress = progress self._changeEvent.set() @@ -614,6 +765,7 @@ class StateMonitor(object): "state": self._state, "job": self._jobData, "gcode": self._gcodeData, + "sdUpload": self._sdUploadData, "currentZ": self._currentZ, "progress": self._progress } diff --git a/octoprint/server.py b/octoprint/server.py index 5358ad0..ef21eb9 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -126,7 +126,8 @@ def index(): enableTimelapse=(settings().get(["webcam", "snapshot"]) is not None and settings().get(["webcam", "ffmpeg"]) is not None), enableGCodeVisualizer=settings().get(["feature", "gCodeVisualizer"]), enableSystemMenu=settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0, - enableAccessControl=userManager is not None + enableAccessControl=userManager is not None, + enableSdSupport=settings().get(["feature", "sdSupport"]) ) #~~ Printer control @@ -261,11 +262,40 @@ def getCustomControls(): customControls = settings().get(["controls"]) return jsonify(controls=customControls) +@app.route(BASEURL + "control/sd", methods=["POST"]) +@login_required +def sdCommand(): + if not settings().getBoolean(["feature", "sdSupport"]) or not printer.isOperational() or printer.isPrinting(): + return jsonify(SUCCESS) + + if "command" in request.values.keys(): + command = request.values["command"] + if command == "init": + printer.initSdCard() + elif command == "refresh": + printer.refreshSdFiles() + elif command == "release": + printer.releaseSdCard() + + return jsonify(SUCCESS) + #~~ GCODE file handling @app.route(BASEURL + "gcodefiles", methods=["GET"]) def readGcodeFiles(): - return jsonify(files=gcodeManager.getAllFileData()) + files = gcodeManager.getAllFileData() + + sdFileList = printer.getSdFiles() + if sdFileList is not None: + for sdFile in sdFileList: + files.append({ + "name": sdFile, + "size": "n/a", + "bytes": 0, + "date": "n/a", + "origin": "sd" + }) + return jsonify(files=files) @app.route(BASEURL + "gcodefiles/", methods=["GET"]) def readGcodeFile(filename): @@ -278,6 +308,8 @@ def uploadGcodeFile(): if "gcode_file" in request.files.keys(): file = request.files["gcode_file"] filename = gcodeManager.addFile(file) + if filename and "target" in request.values.keys() and request.values["target"] == "sd": + printer.addSdFile(filename, gcodeManager.getAbsolutePath(filename)) global eventManager eventManager.fire("Upload", filename) @@ -290,12 +322,17 @@ def loadGcodeFile(): printAfterLoading = False if "print" in request.values.keys() and request.values["print"] in valid_boolean_trues: printAfterLoading = True - filename = gcodeManager.getAbsolutePath(request.values["filename"]) - if filename is not None: - printer.loadGcode(filename, printAfterLoading) - global eventManager - eventManager.fire("LoadStart", filename) + if "target" in request.values.keys() and request.values["target"] == "sd": + filename = request.values["filename"] + printer.selectSdFile(filename, printAfterLoading) + else: + filename = gcodeManager.getAbsolutePath(request.values["filename"]) + if filename is not None: + printer.loadGcode(filename, printAfterLoading) + + global eventManager + eventManager.fire("LoadStart", filename) return jsonify(SUCCESS) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @@ -303,9 +340,17 @@ def loadGcodeFile(): def deleteGcodeFile(): if "filename" in request.values.keys(): filename = request.values["filename"] - gcodeManager.removeFile(filename) + if "target" in request.values.keys() and request.values["target"] == "sd": + printer.deleteSdFile(filename) + else: + gcodeManager.removeFile(filename) return readGcodeFiles() +@app.route(BASEURL + "gcodefiles/refresh", methods=["POST"]) +def refreshFiles(): + printer.updateSdFiles() + return jsonify(SUCCESS) + #~~ timelapse handling @app.route(BASEURL + "timelapse", methods=["GET"]) @@ -394,7 +439,10 @@ def getSettings(): }, "feature": { "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), - "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]) + "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), + "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]), + "resetLineNumbersWithPrefixedN": s.getBoolean(["feature", "resetLineNumbersWithPrefixedN"]), + "sdSupport": s.getBoolean(["feature", "sdSupport"]) }, "folder": { "uploads": s.getBaseFolder("uploads"), @@ -439,6 +487,9 @@ def setSettings(): if "feature" in data.keys(): if "gcodeViewer" in data["feature"].keys(): s.setBoolean(["feature", "gCodeVisualizer"], data["feature"]["gcodeViewer"]) if "waitForStart" in data["feature"].keys(): s.setBoolean(["feature", "waitForStartOnConnect"], data["feature"]["waitForStart"]) + if "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"]) + if "resetLineNumbersWithPrefixedN" in data["feature"].keys(): s.setBoolean(["feature", "resetLineNumbersWithPrefixedN"], data["feature"]["resetLineNumbersWithPrefixedN"]) + if "sdSupport" in data["feature"].keys(): s.setBoolean(["feature", "sdSupport"], data["feature"]["sdSupport"]) if "folder" in data.keys(): if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"]) diff --git a/octoprint/settings.py b/octoprint/settings.py index bf89f66..1d6457a 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -39,13 +39,18 @@ default_settings = { }, "feature": { "gCodeVisualizer": True, - "waitForStartOnConnect": False + "waitForStartOnConnect": False, + "waitForWaitOnConnect": False, + "alwaysSendChecksum": False, + "resetLineNumbersWithPrefixedN": False, + "sdSupport": False }, "folder": { "uploads": None, "timelapse": None, "timelapse_tmp": None, - "logs": None + "logs": None, + "virtualSd": None }, "temperature": { "profiles": diff --git a/octoprint/static/css/octoprint.less b/octoprint/static/css/octoprint.less index 2f8c258..b61e3bb 100644 --- a/octoprint/static/css/octoprint.less +++ b/octoprint/static/css/octoprint.less @@ -111,10 +111,10 @@ body { .octoprint-container { .accordion-heading { - .settings-trigger { + .accordion-heading-button { float: right; - .dropdown-toggle { + a { display: inline-block; padding: 8px 15px; font-size: 14px; @@ -145,6 +145,9 @@ body { padding-right: 4px; } +.upload-buttons .btn { + margin-right: 0; +} /** Tables */ @@ -399,3 +402,127 @@ ul.dropdown-menu li a { overflow: visible !important; } +#drop_overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + display: none; + + &.in { + display: block; + } + + #drop_overlay_background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000000; + filter:alpha(opacity=50); + -moz-opacity:0.5; + -khtml-opacity: 0.5; + opacity: 0.5; + } + + #drop_overlay_wrapper { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + padding-top: 60px; + + @dropzone_width: 400px; + @dropzone_height: 400px; + @dropzone_distance: 50px; + @dropzone_border: 2px; + + #drop, #drop_background { + position: absolute; + top: 50%; + left: 50%; + margin-left: -1 * @dropzone_width / 2; + margin-top: -1 * @dropzone_height / 2; + } + + #drop_locally, #drop_locally_background { + position: absolute; + top: 50%; + left: 50%; + margin-left: -1 * @dropzone_width - @dropzone_distance / 2; + margin-top: -1 * @dropzone_height / 2; + } + + #drop_sd, #drop_sd_background { + position: absolute; + top: 50%; + left: 50%; + margin-left: @dropzone_distance / 2; + margin-top: -1 * @dropzone_height / 2; + } + + .dropzone { + width: @dropzone_width + 2 * @dropzone_border; + height: @dropzone_height + 2 * @dropzone_border; + z-index: 10001; + + color: #ffffff; + font-size: 30px; + + i { + font-size: 50px; + } + + .centered { + display: table-cell; + text-align: center; + vertical-align: middle; + width: @dropzone_width; + height: @dropzone_height; + line-height: 40px; + + filter:alpha(opacity=100); + -moz-opacity:1.0; + -khtml-opacity: 1.0; + opacity: 1.0; + } + } + + .dropzone_background { + width: @dropzone_width; + height: @dropzone_height; + border: 2px dashed #eeeeee; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + + background-color: #000000; + filter:alpha(opacity=25); + -moz-opacity:0.25; + -khtml-opacity: 0.25; + opacity: 0.25; + + &.hover { + background-color: #000000; + filter:alpha(opacity=50); + -moz-opacity:0.5; + -khtml-opacity: 0.5; + opacity: 0.5; + + } + + &.fade { + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; + opacity: 1; + } + } + } +} \ No newline at end of file diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index d2d0882..01a01d8 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -213,12 +213,14 @@ function PrinterStateViewModel(loginStateViewModel) { self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); + self.isSdReady = 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.progress = ko.observable(undefined); self.currentLine = ko.observable(undefined); self.totalLines = ko.observable(undefined); self.currentHeight = ko.observable(undefined); @@ -229,10 +231,10 @@ function PrinterStateViewModel(loginStateViewModel) { var currentLine = self.currentLine() ? self.currentLine() : "-"; return currentLine + " / " + self.totalLines(); }); - self.progress = ko.computed(function() { - if (!self.currentLine() || !self.totalLines()) + self.progressString = ko.computed(function() { + if (!self.progress()) return 0; - return Math.round(self.currentLine() * 100 / self.totalLines()); + return self.progress(); }); self.pauseString = ko.computed(function() { if (self.isPaused()) @@ -253,6 +255,7 @@ function PrinterStateViewModel(loginStateViewModel) { self._processStateData(data.state) self._processJobData(data.job); self._processGcodeData(data.gcode); + self._processSdUploadData(data.sdUpload); self._processProgressData(data.progress); self._processZData(data.currentZ); } @@ -266,6 +269,7 @@ function PrinterStateViewModel(loginStateViewModel) { self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); + self.isSdReady(data.flags.sdReady); } self._processJobData = function(data) { @@ -286,8 +290,20 @@ function PrinterStateViewModel(loginStateViewModel) { } } + self._processSdUploadData = function(data) { + if (self.isLoading()) { + var progress = Math.round(data.progress * 100); + self.filename("Streaming... (" + progress + "%)"); + } + } + self._processProgressData = function(data) { - self.currentLine(data.progress); + if (data.progress) { + self.progress(Math.round(data.progress * 100)); + } else { + self.progress(undefined); + } + self.currentLine(data.currentLine); self.printTime(data.printTime); self.printTimeLeft(data.printTimeLeft); } @@ -742,6 +758,7 @@ function GcodeFilesViewModel(loginStateViewModel) { self.isError = ko.observable(undefined); self.isReady = ko.observable(undefined); self.isLoading = ko.observable(undefined); + self.isSdReady = ko.observable(undefined); // initialize list helper self.listHelper = new ItemListHelper( @@ -754,14 +771,14 @@ function GcodeFilesViewModel(loginStateViewModel) { return 0; }, "upload": function(a, b) { - // sorts descending - if (a["date"] > b["date"]) return -1; - if (a["date"] < b["date"]) return 1; - return 0; + // sorts descending + if (b["date"] === undefined || 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 (b["bytes"] === undefined || a["bytes"] > b["bytes"]) return -1; if (a["bytes"] < b["bytes"]) return 1; return 0; } @@ -769,10 +786,17 @@ function GcodeFilesViewModel(loginStateViewModel) { { "printed": function(file) { return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0); + }, + "sd": function(file) { + return file["origin"] && file["origin"] == "sd"; + }, + "local": function(file) { + return !(file["origin"] && file["origin"] == "sd"); } }, "name", [], + [["sd", "local"]], CONFIG_GCODEFILESPERPAGE ); @@ -800,6 +824,7 @@ function GcodeFilesViewModel(loginStateViewModel) { self.isError(data.flags.error); self.isReady(data.flags.ready); self.isLoading(data.flags.loading); + self.isSdReady(data.flags.sdReady); } self.requestData = function() { @@ -823,24 +848,51 @@ function GcodeFilesViewModel(loginStateViewModel) { } self.loadFile = function(filename, printAfterLoad) { + var file = self.listHelper.getItem(function(item) {return item.name == filename}); + if (!file) return; + $.ajax({ url: AJAX_BASEURL + "gcodefiles/load", type: "POST", dataType: "json", - data: {filename: filename, print: printAfterLoad} + data: {filename: filename, print: printAfterLoad, target: file.origin} }) } self.removeFile = function(filename) { + var file = self.listHelper.getItem(function(item) {return item.name == filename}); + if (!file) return; + $.ajax({ url: AJAX_BASEURL + "gcodefiles/delete", type: "POST", dataType: "json", - data: {filename: filename}, + data: {filename: filename, target: file.origin}, success: self.fromResponse }) } + self.initSdCard = function() { + self._sendSdCommand("init"); + } + + self.releaseSdCard = function() { + self._sendSdCommand("release"); + } + + self.refreshSdFiles = function() { + self._sendSdCommand("refresh"); + } + + self._sendSdCommand = function(command) { + $.ajax({ + url: AJAX_BASEURL + "control/sd", + type: "POST", + dataType: "json", + data: {command: command} + }); + } + self.getPopoverContent = function(data) { var output = "

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

"; if (data["gcodeAnalysis"]) { @@ -917,6 +969,7 @@ function TimelapseViewModel(loginStateViewModel) { }, "name", [], + [], CONFIG_TIMELAPSEFILESPERPAGE ) @@ -996,13 +1049,15 @@ function GcodeViewModel(loginStateViewModel) { self.status = 'idle'; self.enabled = false; + self.errorCount = 0; + self.initialize = function(){ self.enabled = true; GCODE.ui.initHandlers(); } self.loadFile = function(filename){ - if (self.status == 'idle') { + if (self.status == 'idle' && self.errorCount < 3) { self.status = 'request'; $.ajax({ url: AJAX_BASEURL + "gcodefiles/" + filename, @@ -1016,6 +1071,7 @@ function GcodeViewModel(loginStateViewModel) { }, error: function() { self.status = 'idle'; + self.errorCount++; } }) } @@ -1045,6 +1101,7 @@ function GcodeViewModel(loginStateViewModel) { GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd); GCODE.ui.updateLayerInfo(cmdIndex.layer); } + self.errorCount = 0 } else if (data.job.filename) { self.loadFile(data.job.filename); } @@ -1198,7 +1255,7 @@ function UsersViewModel(loginStateViewModel) { if (user === undefined) return; if (user.name == loginStateViewModel.username()) { - // we do not allow to delete ourself + // we do not allow to delete ourselves $.pnotify({title: "Not possible", text: "You may not delete your own account.", type: "error"}); return; } @@ -1267,6 +1324,9 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.feature_gcodeViewer = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined); + self.feature_alwaysSendChecksum = ko.observable(undefined); + self.feature_resetLineNumbersWithPrefixedN = ko.observable(undefined); + self.feature_sdSupport = ko.observable(undefined); self.folder_uploads = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined); @@ -1311,6 +1371,9 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_waitForStart(response.feature.waitForStart); + self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum); + self.feature_resetLineNumbersWithPrefixedN(response.feature.resetLineNumbersWithPrefixedN); + self.feature_sdSupport(response.feature.sdSupport); self.folder_uploads(response.folder.uploads); self.folder_timelapse(response.folder.timelapse); @@ -1343,7 +1406,11 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { }, "feature": { "gcodeViewer": self.feature_gcodeViewer(), - "waitForStart": self.feature_waitForStart() + "waitForStart": self.feature_waitForStart(), + "alwaysSendChecksum": self.feature_alwaysSendChecksum(), + "resetLineNumbersWithPrefixedN": self.feature_resetLineNumbersWithPrefixedN(), + "waitForStart": self.feature_waitForStart(), + "sdSupport": self.feature_sdSupport() }, "folder": { "uploads": self.folder_uploads(), @@ -1427,6 +1494,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM self.timelapseViewModel.requestData(); $("#webcam_image").attr("src", CONFIG_WEBCAM_STREAM + "?" + new Date().getTime()); self.loginStateViewModel.requestData(); + self.gcodeFilesViewModel.requestData(); } }) self._socket.on("disconnect", function() { @@ -1475,7 +1543,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM } } -function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, filesPerPage) { +function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, exclusiveFilters, filesPerPage) { var self = this; self.listType = listType; @@ -1483,6 +1551,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor self.supportedFilters = supportedFilters; self.defaultSorting = defaultSorting; self.defaultFilters = defaultFilters; + self.exclusiveFilters = exclusiveFilters; self.allItems = []; self.items = ko.observableArray([]); @@ -1574,6 +1643,17 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor } } + self.getItem = function(matcher) { + var itemList = self.items(); + for (var i = 0; i < itemList.length; i++) { + if (matcher(itemList[i])) { + return itemList[i]; + } + } + + return undefined; + } + //~~ sorting self.changeSorting = function(sorting) { @@ -1604,6 +1684,16 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor if (!_.contains(_.keys(self.supportedFilters), filter)) return; + for (var i = 0; i < self.exclusiveFilters.length; i++) { + if (_.contains(self.exclusiveFilters[i], filter)) { + for (var j = 0; j < self.exclusiveFilters[i].length; j++) { + if (self.exclusiveFilters[i][j] == filter) + continue; + self.removeFilter(self.exclusiveFilters[i][j]); + } + } + } + var filters = self.currentFilters(); filters.push(filter); self.currentFilters(filters); @@ -1613,7 +1703,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor } self.removeFilter = function(filter) { - if (filter != "printed") + if (!_.contains(_.keys(self.supportedFilters), filter)) return; var filters = self.currentFilters(); @@ -1765,8 +1855,6 @@ $(function() { return false; }) - //~~ Print job control (should move to PrinterStateViewModel) - //~~ Temperature control (should really move to knockout click binding) $("#temp_newTemp_set").click(function() { @@ -1828,23 +1916,103 @@ $(function() { //~~ Gcode upload + function gcode_upload_done(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(""); + } + + function gcode_upload_progress(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 ..."); + } + } + + var localTarget; + if (CONFIG_SD_SUPPORT) { + localTarget = $("#drop_locally"); + } else { + localTarget = $("#drop"); + } + $("#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 ..."); - } + dropZone: localTarget, + formData: {target: "local"}, + done: gcode_upload_done, + progressall: gcode_upload_progress + }); + + if (CONFIG_SD_SUPPORT) { + $("#gcode_upload_sd").fileupload({ + dataType: "json", + dropZone: $("#drop_sd"), + formData: {target: "sd"}, + done: gcode_upload_done, + progressall: gcode_upload_progress + }); + } + + $(document).bind("dragover", function (e) { + var dropOverlay = $("#drop_overlay"); + var dropZone = $("#drop"); + var dropZoneLocal = $("#drop_locally"); + var dropZoneSd = $("#drop_sd"); + var dropZoneBackground = $("#drop_background"); + var dropZoneLocalBackground = $("#drop_locally_background"); + var dropZoneSdBackground = $("#drop_sd_background"); + var timeout = window.dropZoneTimeout; + + if (!timeout) { + dropOverlay.addClass('in'); + } else { + clearTimeout(timeout); } + + var foundLocal = false; + var foundSd = false; + var found = false + var node = e.target; + do { + if (dropZoneLocal && node === dropZoneLocal[0]) { + foundLocal = true; + break; + } else if (dropZoneSd && node === dropZoneSd[0]) { + foundSd = true; + break; + } else if (dropZone && node === dropZone[0]) { + found = true; + break; + } + node = node.parentNode; + } while (node != null); + + if (foundLocal) { + dropZoneLocalBackground.addClass("hover"); + dropZoneSdBackground.removeClass("hover"); + } else if (foundSd) { + dropZoneSdBackground.addClass("hover"); + dropZoneLocalBackground.removeClass("hover"); + } else if (found) { + dropZoneBackground.addClass("hover"); + } else { + if (dropZoneLocalBackground) dropZoneLocalBackground.removeClass("hover"); + if (dropZoneSdBackground) dropZoneSdBackground.removeClass("hover"); + if (dropZoneBackground) dropZoneBackground.removeClass("hover"); + } + + window.dropZoneTimeout = setTimeout(function () { + window.dropZoneTimeout = null; + dropOverlay.removeClass("in"); + if (dropZoneLocal) dropZoneLocalBackground.removeClass("hover"); + if (dropZoneSd) dropZoneSdBackground.removeClass("hover"); + if (dropZone) dropZoneBackground.removeClass("hover"); + }, 100); }); //~~ Offline overlay @@ -1908,7 +2076,7 @@ $(function() { } else { $("#gcode_upload").fileupload("disable"); } - }) + }); //~~ UI stuff @@ -1930,10 +2098,13 @@ $(function() { } // Fix input element click problem on login dialog - $('.dropdown input, .dropdown label').click(function(e) { + $(".dropdown input, .dropdown label").click(function(e) { e.stopPropagation(); }); + $(document).bind("drop dragover", function (e) { + e.preventDefault(); + }); } ); diff --git a/octoprint/templates/dialogs.jinja2 b/octoprint/templates/dialogs.jinja2 index 6c95967..b9244f0 100644 --- a/octoprint/templates/dialogs.jinja2 +++ b/octoprint/templates/dialogs.jinja2 @@ -17,6 +17,21 @@ +
+
+
+ {% if enableSdSupport %} +

Upload locally
+
+

Upload to SD
+
+ {% else %} +

Upload
+
+ {% endif %} +
+
+