From 039a17d923da953644309574a0b62c3532aa7e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 20 May 2013 19:18:03 +0200 Subject: [PATCH] First try at SD card support --- octoprint/printer.py | 155 ++++++++++-- octoprint/server.py | 39 +++- octoprint/settings.py | 6 +- octoprint/static/js/ui.js | 111 +++++++-- octoprint/templates/index.jinja2 | 13 +- octoprint/templates/settings.jinja2 | 7 + octoprint/timelapse.py | 3 - octoprint/util/comm.py | 351 ++++++++++++++++++++++++---- 8 files changed, 598 insertions(+), 87 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index be080b2..5f837be 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -62,6 +62,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"} @@ -86,6 +91,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 ) @@ -120,7 +126,12 @@ class Printer(): try: callback.sendCurrentData(copy.deepcopy(data)) except: pass -#~~ printer commands + def _sendTriggerUpdateCallbacks(self, type): + for callback in self._callbacks: + try: callback.sendUpdateTrigger(type) + except: pass + + #~~ printer commands def connect(self, port=None, baudrate=None): """ @@ -184,13 +195,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): """ @@ -206,13 +223,17 @@ 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 self._gcodeManager.printFailed(self._filename) @@ -250,7 +271,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 @@ -263,7 +284,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) @@ -365,13 +386,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. @@ -379,18 +399,22 @@ 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() + newProgress = float(filePos) / float(fileSize) + else: + newLine = self._comm.getPrintPos() + newProgress = float(newLine) / float(len(self._gcodeList)) - 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): """ @@ -402,6 +426,66 @@ class Printer(): self._setCurrentZ(newZ) + 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) + + #~~ callbacks triggered by sdFileStreamer + + def _onSdFileStreamProgress(self, filename, progress): + self._stateMonitor.setSdUploadData({"filename": filename, "progress": progress}) + + def _onSdFileStreamFinish(self, filename): + self._setJobData(filename, None) + 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): @@ -414,7 +498,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}) @@ -464,10 +548,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 class GcodeLoader(threading.Thread): """ @@ -514,6 +598,34 @@ 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: + self._comm.sendCommand(line) + self._progressCallback(sdFilename, float(f.tell()) / float(size)) + time.sleep(0.001) # do not send too fast + finally: + self._comm.endSdFileTransfer(sdFilename) + self._finishCallback(sdFilename) + class StateMonitor(object): def __init__(self, ratelimit, updateCallback, addTemperatureCallback, addLogCallback, addMessageCallback): self._ratelimit = ratelimit @@ -525,6 +637,7 @@ class StateMonitor(object): self._state = None self._jobData = None self._gcodeData = None + self._sdUploadData = None self._currentZ = None self._progress = None @@ -535,10 +648,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) @@ -570,6 +684,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() @@ -594,6 +712,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 3a6249a..7cc3194 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -118,7 +118,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 @@ -257,7 +258,19 @@ def getCustomControls(): @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): @@ -270,6 +283,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)) return jsonify(files=gcodeManager.getAllFileData(), filename=filename) @app.route(BASEURL + "gcodefiles/load", methods=["POST"]) @@ -279,9 +294,14 @@ 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) + + 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) return jsonify(SUCCESS) @app.route(BASEURL + "gcodefiles/delete", methods=["POST"]) @@ -289,7 +309,10 @@ 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() #~~ timelapse handling @@ -380,7 +403,8 @@ def getSettings(): }, "feature": { "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), - "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]) + "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), + "sdSupport": s.getBoolean(["feature", "sdSupport"]) }, "folder": { "uploads": s.getBaseFolder("uploads"), @@ -424,6 +448,7 @@ 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 "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 7c385f8..5bfbf79 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -39,13 +39,15 @@ default_settings = { }, "feature": { "gCodeVisualizer": True, - "waitForStartOnConnect": False + "waitForStartOnConnect": False, + "sdSupport": False }, "folder": { "uploads": None, "timelapse": None, "timelapse_tmp": None, - "logs": None + "logs": None, + "virtualSd": None }, "temperature": { "profiles": diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index d2d0882..cd794d4 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -219,6 +219,7 @@ function PrinterStateViewModel(loginStateViewModel) { 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 +230,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 +254,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); } @@ -286,8 +288,20 @@ function PrinterStateViewModel(loginStateViewModel) { } } + self._processSdUploadData = function(data) { + if (self.isLoading()) { + var progress = Math.round(data.progress * 100); + self.filename("Streaming to SD... (" + 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); } @@ -754,14 +768,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 +783,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 ); @@ -823,20 +844,26 @@ 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 }) } @@ -917,6 +944,7 @@ function TimelapseViewModel(loginStateViewModel) { }, "name", [], + [], CONFIG_TIMELAPSEFILESPERPAGE ) @@ -996,13 +1024,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 +1046,7 @@ function GcodeViewModel(loginStateViewModel) { }, error: function() { self.status = 'idle'; + self.errorCount++; } }) } @@ -1045,6 +1076,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 +1230,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 +1299,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.feature_gcodeViewer = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined); + self.feature_sdSupport = ko.observable(undefined); self.folder_uploads = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined); @@ -1311,6 +1344,7 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_waitForStart(response.feature.waitForStart); + self.feature_sdSupport(response.feature.sdSupport); self.folder_uploads(response.folder.uploads); self.folder_timelapse(response.folder.timelapse); @@ -1343,7 +1377,8 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) { }, "feature": { "gcodeViewer": self.feature_gcodeViewer(), - "waitForStart": self.feature_waitForStart() + "waitForStart": self.feature_waitForStart(), + "sdSupport": self.feature_sdSupport() }, "folder": { "uploads": self.folder_uploads(), @@ -1475,7 +1510,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 +1518,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 +1610,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 +1651,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 +1670,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(); @@ -1847,6 +1904,26 @@ $(function() { } }); + $("#gcode_upload_sd").fileupload({ + dataType: "json", + formData: {target: "sd"}, + 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()}); @@ -1908,7 +1985,7 @@ $(function() { } else { $("#gcode_upload").fileupload("disable"); } - }) + }); //~~ UI stuff diff --git a/octoprint/templates/index.jinja2 b/octoprint/templates/index.jinja2 index df10b19..d8ebcfe 100644 --- a/octoprint/templates/index.jinja2 +++ b/octoprint/templates/index.jinja2 @@ -24,6 +24,7 @@ var CONFIG_USERSPERPAGE = 10; var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}"; var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %} + var CONFIG_SD_SUPPORT = {% if enableSdSupport -%} true; {% else %} false; {%- endif %} var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/socket.io/WebSocketMain.swf') }}"; var WEB_SOCKET_DEBUG = true; @@ -116,7 +117,7 @@ Print Time Left:
-
+
+
+
+ +
+
diff --git a/octoprint/timelapse.py b/octoprint/timelapse.py index 2db8984..3f2c1d6 100644 --- a/octoprint/timelapse.py +++ b/octoprint/timelapse.py @@ -53,9 +53,6 @@ class Timelapse(object): def onPrintjobStopped(self): self.stopTimelapse() - def onPrintjobProgress(self, oldPos, newPos, percentage): - pass - def onZChange(self, oldZ, newZ): pass diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 9ef8893..8241d5e 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -64,9 +64,24 @@ class VirtualPrinter(): self.bedTemp = 1.0 self.bedTargetTemp = 1.0 + self._virtualSd = settings().getBaseFolder("virtualSd") + self._sdPrinter = None + self._sdPrintingSemaphore = threading.Event() + self._selectedSdFile = None + self._selectedSdFileSize = None + self._selectedSdFilePos = None + self._writingToSd = False + def write(self, data): if self.readList is None: return + + # shortcut for writing to SD + if self._writingToSd and not self._selectedSdFile is None and not "M29" in data: + with open(self._selectedSdFile, "a") as f: + f.write(data) + return + #print "Send: %s" % (data.rstrip()) if 'M104' in data or 'M109' in data: try: @@ -78,11 +93,121 @@ class VirtualPrinter(): self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1)) except: pass + if 'M105' in data: self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp)) + elif 'M20' in data: + self._listSd() + elif 'M23' in data: + filename = data.split(None, 1)[1].strip() + self._selectSdFile(filename) + elif 'M24' in data: + self._startSdPrint() + elif 'M25' in data: + self._pauseSdPrint() + elif 'M27' in data: + self._reportSdStatus() + elif 'M28' in data: + filename = data.split(None, 1)[1].strip() + self._writeSdFile(filename) + elif 'M29' in data: + self._finishSdFile() + elif 'M30' in data: + filename = data.split(None, 1)[1].strip() + self._deleteSdFile(filename) elif len(data.strip()) > 0: self.readList.append("ok\n") + def _listSd(self): + self.readList.append("Begin file list") + for osFile in os.listdir(self._virtualSd): + self.readList.append(osFile.upper()) + self.readList.append("End file list") + self.readList.append("ok") + + def _selectSdFile(self, filename): + file = os.path.join(self._virtualSd, filename).lower() + if not os.path.exists(file) or not os.path.isfile(file): + self.readList.append("open failed, File: %s." % filename) + else: + self._selectedSdFile = file + self._selectedSdFileSize = os.stat(file).st_size + self.readList.append("File opened: %s Size: %d" % (filename, self._selectedSdFileSize)) + self.readList.append("File selected") + + def _startSdPrint(self): + if self._selectedSdFile is not None: + if self._sdPrinter is None: + self._sdPrinter = threading.Thread(target=self._sdPrintingWorker) + self._sdPrinter.start() + self._sdPrintingSemaphore.set() + self.readList.append("ok") + + def _pauseSdPrint(self): + self._sdPrintingSemaphore.clear() + self.readList.append("ok") + + def _reportSdStatus(self): + if self._sdPrinter is not None and self._sdPrintingSemaphore.is_set: + self.readList.append("SD printing byte %d/%d" % (self._selectedSdFilePos, self._selectedSdFileSize)) + else: + self.readList.append("Not SD printing") + + def _writeSdFile(self, filename): + file = os.path.join(self._virtualSd, filename).lower() + if os.path.exists(file): + if os.path.isfile(file): + os.remove(file) + else: + self.readList.append("error writing to file") + + self._writingToSd = True + self._selectedSdFile = file + self.readList.append("ok") + + def _finishSdFile(self): + self._writingToSd = False + self._selectedSdFile = None + self.readList.append("ok") + + def _sdPrintingWorker(self): + self._selectedSdFilePos = 0 + with open(self._selectedSdFile, "rb") as f: + for line in f: + # read current file position + print("Progress: %d (%d / %d)" % ((round(self._selectedSdFilePos * 100 / self._selectedSdFileSize)), self._selectedSdFilePos, self._selectedSdFileSize)) + self._selectedSdFilePos = f.tell() + + # if we are paused, wait for unpausing + self._sdPrintingSemaphore.wait() + + # set target temps + if 'M104' in line or 'M109' in line: + try: + self.targetTemp = float(re.search('S([0-9]+)', line).group(1)) + except: + pass + if 'M140' in line or 'M190' in line: + try: + self.bedTargetTemp = float(re.search('S([0-9]+)', line).group(1)) + except: + pass + + print line + + time.sleep(0.01) + + self._sdPrintingSemaphore.clear() + self._selectedSdFilePos = 0 + self._sdPrinter = None + self.readList.append("Done printing file") + + def _deleteSdFile(self, filename): + file = os.path.join(self._virtualSd, filename) + if os.path.exists(file) and os.path.isfile(file): + os.remove(file) + self.readList.append("ok") + def readline(self): if self.readList is None: return '' @@ -105,7 +230,6 @@ class VirtualPrinter(): if self.readList is None: return '' time.sleep(0.001) - #print "Recv: %s" % (self.readList[0].rstrip()) return self.readList.pop(0) def close(self): @@ -124,12 +248,21 @@ class MachineComPrintCallback(object): def mcMessage(self, message): pass - def mcProgress(self, lineNr): + def mcProgress(self): pass - + def mcZChange(self, newZ): pass + def mcSdFiles(self, files): + pass + + def mcSdSelected(self, filename, size): + pass + + def mcSdPrintingDone(self): + pass + class MachineCom(object): STATE_NONE = 0 STATE_OPEN_SERIAL = 1 @@ -142,6 +275,7 @@ class MachineCom(object): STATE_CLOSED = 8 STATE_ERROR = 9 STATE_CLOSED_WITH_ERROR = 10 + STATE_RECEIVING_FILE = 11 def __init__(self, port = None, baudrate = None, callbackObject = None): self._logger = logging.getLogger(__name__) @@ -177,12 +311,19 @@ class MachineCom(object): self._currentZ = -1 self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 - self._printStartTime100 = None + self._printStartTime = None self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() - + + self._sdPrinting = False + self._sdFileList = False + self._sdFile = None + self._sdFilePos = None + self._sdFileSize = None + self._sdFiles = [] + def _changeState(self, newState): if self._state == newState: return @@ -208,7 +349,10 @@ class MachineCom(object): if self._state == self.STATE_OPERATIONAL: return "Operational" if self._state == self.STATE_PRINTING: - return "Printing" + if self._sdPrinting: + return "Printing from SD" + else: + return "Printing" if self._state == self.STATE_PAUSED: return "Paused" if self._state == self.STATE_CLOSED: @@ -217,6 +361,8 @@ class MachineCom(object): return "Error: %s" % (self.getShortErrorString()) if self._state == self.STATE_CLOSED_WITH_ERROR: return "Error: %s" % (self.getShortErrorString()) + if self._state == self.STATE_RECEIVING_FILE: + return "Streaming file to SD" return "?%d?" % (self._state) def getShortErrorString(self): @@ -234,31 +380,57 @@ class MachineCom(object): return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR def isOperational(self): - return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED + return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED or self._state == self.STATE_RECEIVING_FILE def isPrinting(self): return self._state == self.STATE_PRINTING + + def isSdPrinting(self): + return self._sdPrinting def isPaused(self): return self._state == self.STATE_PAUSED + def isBusy(self): + return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE + def getPrintPos(self): - return self._gcodePos + if self._sdPrinting: + return self._sdFilePos + else: + return self._gcodePos def getPrintTime(self): - if self._printStartTime100 == None: + if self._printStartTime == None: return 0 else: - return time.time() - self._printStartTime100 + return time.time() - self._printStartTime def getPrintTimeRemainingEstimate(self): - if self._printStartTime100 == None or self.getPrintPos() < 200: + if self._printStartTime == None: return None - printTime = (time.time() - self._printStartTime100) / 60 - printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100) - printTimeLeft = printTimeTotal - printTime - return printTimeLeft - + + if self._sdPrinting: + printTime = (time.time() - self._printStartTime) / 60 + if self._sdFilePos > 0: + printTimeTotal = printTime * (self._sdFileSize / self._sdFilePos) + else: + printTimeTotal = printTime * self._sdFileSize + printTimeLeft = printTimeTotal - printTime + return printTimeLeft + else: + # for host printing we only start counting the print time at gcode line 100, so we need to calculate stuff + # a bit different here + if self.getPrintPos() < 200: + return None + printTime = (time.time() - self._printStartTime) / 60 + printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100) + printTimeLeft = printTimeTotal - printTime + return printTimeLeft + + def getSdProgress(self): + return (self._sdFilePos, self._sdFileSize) + def getTemp(self): return self._temp @@ -318,13 +490,15 @@ class MachineCom(object): #Start monitoring the serial port. timeout = time.time() + 5 tempRequestTimeout = timeout + sdStatusRequestTimeout = timeout startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) while True: line = self._readline() if line == None: break - - #No matter the state, if we see an error, goto the error state and store the error for reference. + + ##~~ Error handling + # No matter the state, if we see an error, goto the error state and store the error for reference. if line.startswith('Error:'): #Oh YEAH, consistency. # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" @@ -338,6 +512,14 @@ class MachineCom(object): elif not self.isError(): self._errorValue = line[6:] self._changeState(self.STATE_ERROR) + + ##~~ SD file list + # if we are currently receiving an sd file list, each line is just a filename, so just read it and abort processing + if self._sdFileList and not 'End file list' in line: + self._sdFiles.append(line) + continue + + ##~~ Temperature processing if ' T:' in line or line.startswith('T:'): self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0)) if ' B:' in line: @@ -348,6 +530,36 @@ class MachineCom(object): t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t + + ##~~ SD Card handling + elif 'Begin file list' in line: + self._sdFiles = [] + self._sdFileList = True + elif 'End file list' in line: + self._sdFileList = False + self._callback.mcSdFiles(self._sdFiles) + elif 'SD printing byte' in line: + # answer to M27, at least on Marlin, Repetier and Sprinter: "SD printing byte %d/%d" + match = re.search("([0-9]*)/([0-9]*)", line) + self._sdFilePos = int(match.group(1)) + self._sdFileSize = int(match.group(2)) + self._callback.mcProgress() + elif 'File opened' in line: + # answer to M23, at least on Marlin, Repetier and Sprinter: "File opened: %s Size: %d" + match = re.search("File opened: (.*?) Size: ([0-9]*)", line) + self._sdFile = match.group(1) + self._sdFileSize = int(match.group(2)) + elif 'File selected' in line: + # final answer to M23, at least on Marlin, Repetier and Sprinter: "File selected" + self._callback.mcSdSelected(self._sdFile, self._sdFileSize) + elif 'Done printing file' in line: + # printer is reporting file finished printing + self._sdPrinting = False + self._sdFilePos = 0 + self._changeState(self.STATE_OPERATIONAL) + self._callback.mcSdPrintingDone() + + ##~~ Message handling elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): self._callback.mcMessage(line) @@ -395,6 +607,8 @@ class MachineCom(object): startSeen = True elif 'ok' in line and startSeen: self._changeState(self.STATE_OPERATIONAL) + if settings().get(["feature", "sdSupport"]): + self._sendCommand("M20") elif time.time() > timeout: self.close() elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: @@ -403,25 +617,35 @@ class MachineCom(object): self._sendCommand("M105") tempRequestTimeout = time.time() + 5 elif self._state == self.STATE_PRINTING: - if line == '' and time.time() > timeout: - self._log("Communication timeout during printing, forcing a line") - line = 'ok' - #Even when printing request the temperture every 5 seconds. + # Even when printing request the temperture every 5 seconds. if time.time() > tempRequestTimeout: self._commandQueue.put("M105") tempRequestTimeout = time.time() + 5 - if 'ok' in line: - timeout = time.time() + 5 + + if self._sdPrinting: + if time.time() > sdStatusRequestTimeout: + self._commandQueue.put("M27") + sdStatusRequestTimeout = time.time() + 1 + if not self._commandQueue.empty(): self._sendCommand(self._commandQueue.get()) - else: - self._sendNext() - elif "resend" in line.lower() or "rs" in line: - try: - self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) - except: - if "rs" in line: - self._gcodePos = int(line.split()[1]) + else: + if line == '' and time.time() > timeout: + self._log("Communication timeout during printing, forcing a line") + line = 'ok' + + if 'ok' in line: + timeout = time.time() + 5 + if not self._commandQueue.empty(): + self._sendCommand(self._commandQueue.get()) + else: + self._sendNext() + elif "resend" in line.lower() or "rs" in line: + try: + self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) + except: + if "rs" in line: + self._gcodePos = int(line.split()[1]) self._log("Connection closed, closing down monitor") def _log(self, message): @@ -501,7 +725,7 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) return if self._gcodePos == 100: - self._printStartTime100 = time.time() + self._printStartTime = time.time() line = self._gcodeList[self._gcodePos] if type(line) is tuple: self._printSection = line[1] @@ -522,7 +746,7 @@ class MachineCom(object): checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line))) self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum)) self._gcodePos += 1 - self._callback.mcProgress(self._gcodePos) + self._callback.mcProgress() def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') @@ -536,25 +760,50 @@ class MachineCom(object): return self._gcodeList = gcodeList self._gcodePos = 0 - self._printStartTime100 = None self._printSection = 'CUSTOM' self._changeState(self.STATE_PRINTING) self._printStartTime = time.time() for i in xrange(0, 6): self._sendNext() - + + def selectSdFile(self, filename): + if not self.isOperational() or self.isPrinting(): + return + self._sdFile = None + self._sdFilePos = 0 + + self.sendCommand("M23 %s" % filename) + + def printSdFile(self): + if not self.isOperational() or self.isPrinting(): + return + + self.sendCommand("M24") + + self._printSection = 'CUSTOM' + self._sdPrinting = True + self._changeState(self.STATE_PRINTING) + self._printStartTime = time.time() + def cancelPrint(self): if self.isOperational(): self._changeState(self.STATE_OPERATIONAL) + if self._sdPrinting: + self.sendCommand("M25") def setPause(self, pause): if not pause and self.isPaused(): self._changeState(self.STATE_PRINTING) - for i in xrange(0, 6): - self._sendNext() + if self._sdPrinting: + self.sendCommand("M24") + else: + for i in xrange(0, 6): + self._sendNext() if pause and self.isPrinting(): self._changeState(self.STATE_PAUSED) - + if self._sdPrinting: + self.sendCommand("M25") + def setFeedrateModifier(self, type, value): self._feedRateModifier[type] = value @@ -563,6 +812,30 @@ class MachineCom(object): result.update(self._feedRateModifier) return result + def enableSdPrinting(self, enable): + if self.isPrinting(): + return + + self._sdPrinting = enable + + def getSdFiles(self): + return self._sdFiles + + def startSdFileTransfer(self, filename): + if self.isPrinting() or self.isPaused(): + return + self._changeState(self.STATE_RECEIVING_FILE) + self.sendCommand("M28 %s" % filename) + + def endSdFileTransfer(self, filename): + self.sendCommand("M29 %s" % filename) + self._changeState(self.STATE_OPERATIONAL) + self.sendCommand("M20") + + def deleteSdFile(self, filename): + self.sendCommand("M30 %s" % filename) + self.sendCommand("M20") + def getExceptionString(): locationInfo = traceback.extract_tb(sys.exc_info()[2])[0] return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])