From b645073f1d28158968f4a2e7002f3f24dfba51f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 26 May 2013 18:53:43 +0200 Subject: [PATCH] Added SD state evaluation and SD commands --- octoprint/printer.py | 26 ++++- octoprint/server.py | 22 +++++ octoprint/static/css/octoprint.less | 4 +- octoprint/static/js/ui.js | 25 +++++ octoprint/templates/index.jinja2 | 15 ++- octoprint/templates/settings.jinja2 | 14 +-- octoprint/util/comm.py | 145 ++++++++++++++++++++-------- 7 files changed, 200 insertions(+), 51 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index e944ba6..db41c8d 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -348,6 +348,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(), @@ -355,7 +360,8 @@ class Printer(): "error": self.isError(), "loading": self.isLoading(), "paused": self.isPaused(), - "ready": self.isReady() + "ready": self.isReady(), + "sdReady": sdReady } #~~ callbacks triggered from self._comm @@ -434,6 +440,9 @@ 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") @@ -480,6 +489,21 @@ class Printer(): 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): diff --git a/octoprint/server.py b/octoprint/server.py index 96fe006..f7ee412 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -254,6 +254,23 @@ 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"]) @@ -315,6 +332,11 @@ def deleteGcodeFile(): 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"]) diff --git a/octoprint/static/css/octoprint.less b/octoprint/static/css/octoprint.less index 29c0bb5..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; diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index e1c7b75..01a01d8 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -213,6 +213,7 @@ 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); @@ -268,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) { @@ -756,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( @@ -821,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() { @@ -868,6 +872,27 @@ function GcodeFilesViewModel(loginStateViewModel) { }) } + 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"]) { diff --git a/octoprint/templates/index.jinja2 b/octoprint/templates/index.jinja2 index 71c5a1d..7b3c28c 100644 --- a/octoprint/templates/index.jinja2 +++ b/octoprint/templates/index.jinja2 @@ -132,7 +132,7 @@
Files -
+ + + {% if enableSdSupport %} + + {% endif %}
diff --git a/octoprint/templates/settings.jinja2 b/octoprint/templates/settings.jinja2 index 721b6a2..c6a3652 100644 --- a/octoprint/templates/settings.jinja2 +++ b/octoprint/templates/settings.jinja2 @@ -100,6 +100,13 @@
+
+
+ +
+
-
-
- -
-
diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 6ca2c89..8863129 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -57,7 +57,7 @@ def baudrateList(): class VirtualPrinter(): def __init__(self): - self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n'] + self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n', 'SD init fail\n'] # no sd card as default startup scenario self.temp = 0.0 self.targetTemp = 0.0 self.lastTempAt = time.time() @@ -65,6 +65,7 @@ class VirtualPrinter(): self.bedTargetTemp = 1.0 self._virtualSd = settings().getBaseFolder("virtualSd") + self._sdCardReady = False self._sdPrinter = None self._sdPrintingSemaphore = threading.Event() self._selectedSdFile = None @@ -104,27 +105,41 @@ class VirtualPrinter(): # send simulated temperature 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() + if self._sdCardReady: + self._listSd() + elif 'M21' in data: + self._sdCardReady = True + self.readList.append("SD card ok") + elif 'M22' in data: + self._sdCardReady = False elif 'M23' in data: - filename = data.split(None, 1)[1].strip() - self._selectSdFile(filename) + if self._sdCardReady: + filename = data.split(None, 1)[1].strip() + self._selectSdFile(filename) elif 'M24' in data: - self._startSdPrint() + if self._sdCardReady: + self._startSdPrint() elif 'M25' in data: - self._pauseSdPrint() + if self._sdCardReady: + self._pauseSdPrint() elif 'M26' in data: - pos = int(re.search("S([0-9]+)", data).group(1)) - self._setSdPos(pos) + if self._sdCardReady: + pos = int(re.search("S([0-9]+)", data).group(1)) + self._setSdPos(pos) elif 'M27' in data: - self._reportSdStatus() + if self._sdCardReady: + self._reportSdStatus() elif 'M28' in data: - filename = data.split(None, 1)[1].strip() - self._writeSdFile(filename) + if self._sdCardReady: + filename = data.split(None, 1)[1].strip() + self._writeSdFile(filename) elif 'M29' in data: - self._finishSdFile() + if self._sdCardReady: + self._finishSdFile() elif 'M30' in data: - filename = data.split(None, 1)[1].strip() - self._deleteSdFile(filename) + if self._sdCardReady: + filename = data.split(None, 1)[1].strip() + self._deleteSdFile(filename) elif "M110" in data: # reset current line self.currentLine = int(re.search('N([0-9]+)', data).group(1)) @@ -196,7 +211,7 @@ class VirtualPrinter(): def _sdPrintingWorker(self): self._selectedSdFilePos = 0 - with open(self._selectedSdFile, "rb") as f: + with open(self._selectedSdFile, "r") as f: for line in f: # reset position if requested by client if self._newSdFilePos is not None: @@ -284,6 +299,9 @@ class MachineComPrintCallback(object): def mcZChange(self, newZ): pass + def mcSdStateChange(self, sdReady): + pass + def mcSdFiles(self, files): pass @@ -355,6 +373,7 @@ class MachineCom(object): self.thread.daemon = True self.thread.start() + self._sdAvailable = False self._sdPrinting = False self._sdFileList = False self._sdFile = None @@ -443,6 +462,9 @@ class MachineCom(object): def isBusy(self): return self.isPrinting() or self._state == self.STATE_RECEIVING_FILE + def isSdReady(self): + return self._sdAvailable + def getPrintPos(self): if self._sdPrinting: return self._sdFilePos @@ -462,10 +484,11 @@ class MachineCom(object): if self._sdPrinting: printTime = (time.time() - self._printStartTime) / 60 if self._sdFilePos > 0: - printTimeTotal = printTime * (self._sdFileSize / self._sdFilePos) + printTimeTotal = printTime * self._sdFileSize / self._sdFilePos else: printTimeTotal = printTime * self._sdFileSize printTimeLeft = printTimeTotal - printTime + self._logger.info("printTime: %f, sdFileSize: %f, sdFilePos: %f, printTimeTotal: %f, printTimeLeft: %f" % (printTime, self._sdFileSize, self._sdFilePos, printTimeTotal, printTimeLeft)) return printTimeLeft else: # for host printing we only start counting the print time at gcode line 100, so we need to calculate stuff @@ -593,6 +616,14 @@ class MachineCom(object): self._heatupWaitStartTime = t ##~~ SD Card handling + elif 'SD init fail' in line: + self._sdAvailable = False + self._sdFiles = [] + self._callback.mcSdStateChange(self._sdAvailable) + elif 'SD card ok' in line: + self._sdAvailable = True + self.refreshSdFiles() + self._callback.mcSdStateChange(self._sdAvailable) elif 'Begin file list' in line: self._sdFiles = [] self._sdFileList = True @@ -671,8 +702,6 @@ 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() @@ -697,24 +726,28 @@ class MachineCom(object): self._log("Communication timeout during printing, forcing a line") line = 'ok' - # Even when printing request the temperature every 5 seconds. - if time.time() > tempRequestTimeout: - self._commandQueue.put("M105") - tempRequestTimeout = time.time() + 5 - if self._sdPrinting: + if time.time() > tempRequestTimeout: + self._sendCommand("M105") + tempRequestTimeout = time.time() + 5 + if time.time() > sdStatusRequestTimeout: - self._commandQueue.put("M27") + self._sendCommand("M27") sdStatusRequestTimeout = time.time() + 1 - if 'ok' in line and not self._commandQueue.empty(): - self._sendCommand(self._commandQueue.get()) + if 'ok' or 'SD printing byte' in line: + timeout = time.time() + 5 else: + # Even when printing request the temperature 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._resendDelta is not None: - self._resendNextCommand() - elif not self._commandQueue.empty(): + if self._resendDelta is not None: + self._resendNextCommand() + elif not self._commandQueue.empty(): self._sendCommand(self._commandQueue.get()) else: self._sendNext() @@ -908,7 +941,7 @@ class MachineCom(object): self._log("Unexpected error: %s" % (getExceptionString())) self._sendCommand(line, True) self._gcodePos += 1 - self._callback.mcProgress(self._gcodePos) + self._callback.mcProgress() def sendCommand(self, cmd): cmd = cmd.encode('ascii', 'replace') @@ -929,14 +962,6 @@ class MachineCom(object): self._printStartTime = time.time() 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.lower()) - def printSdFile(self): if not self.isOperational() or self.isPrinting(): return @@ -979,24 +1004,64 @@ class MachineCom(object): result.update(self._feedRateModifier) return result + ##~~ SD card def getSdFiles(self): return self._sdFiles def startSdFileTransfer(self, filename): - if self.isPrinting() or self.isPaused(): + if not self.isOperational() or self.isPrinting() or self.isPaused(): return + self._changeState(self.STATE_RECEIVING_FILE) self.sendCommand("M28 %s" % filename.lower()) def endSdFileTransfer(self, filename): + if not self.isOperational() or self.isPrinting() or self.isPaused(): + return + self.sendCommand("M29 %s" % filename.lower()) self._changeState(self.STATE_OPERATIONAL) - self.sendCommand("M20") + self.refreshSdFiles() + + def selectSdFile(self, filename): + if not self.isOperational() or self.isPrinting() or self.isPaused(): + return + + self._sdFile = None + self._sdFilePos = 0 + + self.sendCommand("M23 %s" % filename.lower()) def deleteSdFile(self, filename): + if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdFile == filename.lower()): + # do not delete a file from sd we are currently printing from + return + self.sendCommand("M30 %s" % filename.lower()) + self.refreshSdFiles() + + def refreshSdFiles(self): + if not self.isOperational() or self.isPrinting() or self.isPaused(): + return self.sendCommand("M20") + def initSdCard(self): + if not self.isOperational(): + return + self.sendCommand("M21") + + def releaseSdCard(self): + if not self.isOperational() or ((self.isPrinting() or self.isPaused()) and self._sdPrinting): + # do not release the sd card if we are currently printing from it + return + + self.sendCommand("M22") + self._sdAvailable = False + self._sdFiles = [] + + self._callback.mcSdStateChange(self._sdAvailable) + self._callback.mcSdFiles(self._sdFiles) + 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])