From 8ef580cfd90c06f269b3512558c3f797c7aa2cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 16 Mar 2013 01:57:05 +0100 Subject: [PATCH 01/22] Two changes to try to achieve repetier firmware compatibility - Read a "wait" as an empty line in order to send keep alive temperature updates - Added option to add a checksum to all commands. Needed to add current line tracking for this, let's hope that we'll never get out of synch here... --- octoprint/server.py | 4 ++- octoprint/settings.py | 3 +- octoprint/static/js/ui.js | 5 ++- octoprint/templates/settings.html | 7 ++++ octoprint/util/comm.py | 53 +++++++++++++++++++++---------- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index 9a342da..d61d8b5 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -359,7 +359,8 @@ def getSettings(): }, "feature": { "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), - "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]) + "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), + "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]) }, "folder": { "uploads": s.getBaseFolder("uploads"), @@ -401,6 +402,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 "alwaysSendChecksum" in data["feature"].keys(): s.setBoolean(["feature", "alwaysSendChecksum"], data["feature"]["alwaysSendChecksum"]) 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 0c7dfad..93a94d1 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -39,7 +39,8 @@ default_settings = { }, "feature": { "gCodeVisualizer": True, - "waitForStartOnConnect": False + "waitForStartOnConnect": False, + "alwaysSendChecksum": False }, "folder": { "uploads": None, diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 48df520..7b0942d 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -959,6 +959,7 @@ function SettingsViewModel() { self.feature_gcodeViewer = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined); + self.feature_alwaysSendChecksum = ko.observable(undefined); self.folder_uploads = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined); @@ -1003,6 +1004,7 @@ function SettingsViewModel() { self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_waitForStart(response.feature.waitForStart); + self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum); self.folder_uploads(response.folder.uploads); self.folder_timelapse(response.folder.timelapse); @@ -1035,7 +1037,8 @@ function SettingsViewModel() { }, "feature": { "gcodeViewer": self.feature_gcodeViewer(), - "waitForStart": self.feature_waitForStart() + "waitForStart": self.feature_waitForStart(), + "alwaysSendChecksum": self.feature_alwaysSendChecksum() }, "folder": { "uploads": self.folder_uploads(), diff --git a/octoprint/templates/settings.html b/octoprint/templates/settings.html index 9e93f1e..80a4840 100644 --- a/octoprint/templates/settings.html +++ b/octoprint/templates/settings.html @@ -106,6 +106,13 @@ +
+
+ +
+
diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 00d63fe..807b86a 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -174,7 +174,10 @@ class MachineCom(object): self._heatupWaitStartTime = 0 self._heatupWaitTimeLost = 0.0 self._printStartTime100 = None - + + self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) + self._currentLine = 0 + self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() @@ -344,7 +347,7 @@ class MachineCom(object): t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t - elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): + elif line.strip() != '' and line.strip() != 'ok' and not line.startswith("wait") and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): self._callback.mcMessage(line) if self._state == self.STATE_DETECT_BAUDRATE: @@ -385,28 +388,28 @@ class MachineCom(object): else: self._testingBaudrate = False elif self._state == self.STATE_CONNECTING: - if line == '' and startSeen: + if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") - elif 'start' in line: + elif "start" in line: startSeen = True - elif 'ok' in line and startSeen: + elif "ok" in line and startSeen: self._changeState(self.STATE_OPERATIONAL) elif time.time() > timeout: self.close() elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: #Request the temperature on comm timeout (every 5 seconds) when we are not printing. - if line == '': + if line == "" or "wait" in line: self._sendCommand("M105") tempRequestTimeout = time.time() + 5 elif self._state == self.STATE_PRINTING: - if line == '' and time.time() > timeout: + if line == "" and time.time() > timeout: self._log("Communication timeout during printing, forcing a line") - line = 'ok' + line = "ok" #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: + if "ok" in line: timeout = time.time() + 5 if not self._commandQueue.empty(): self._sendCommand(self._commandQueue.get()) @@ -460,7 +463,7 @@ class MachineCom(object): def __del__(self): self.close() - def _sendCommand(self, cmd): + def _sendCommand(self, cmd, sendChecksum=False): cmd = cmd.upper() if self._serial is None: return @@ -476,13 +479,23 @@ class MachineCom(object): self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1)) except: pass - self._log('Send: %s' % (cmd)) + + commandToSend = cmd + self._currentLine += 1 + if sendChecksum or self._alwaysSendChecksum: + lineNumber = self._gcodePos + if self._alwaysSendChecksum: + lineNumber = self._currentLine + checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNumber, cmd))) + commandToSend = "N%d%s*%d" % (lineNumber, cmd, checksum) + + self._log('Send: %s' % (commandToSend)) try: - self._serial.write(cmd + '\n') + self._serial.write(commandToSend + '\n') except serial.SerialTimeoutException: self._log("Serial timeout while writing to serial port, trying again.") try: - self._serial.write(cmd + '\n') + self._serial.write(commandToSend + '\n') except: self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() @@ -491,7 +504,16 @@ class MachineCom(object): self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) - + finally: + if "M110" in cmd: + if " N" in cmd: + try: + self._currentLine = int(re.search("N([0-9]+)", cmd).group(1)) + except: + pass + else: + self._currentLine = 0 + def _sendNext(self): if self._gcodePos >= len(self._gcodeList): self._changeState(self.STATE_OPERATIONAL) @@ -515,8 +537,7 @@ class MachineCom(object): self._callback.mcZChange(z) except: self._log("Unexpected error: %s" % (getExceptionString())) - 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._sendCommand(line, True) self._gcodePos += 1 self._callback.mcProgress(self._gcodePos) From 5e22b4b096585bf58ca83dc1e643b28f49e65119 Mon Sep 17 00:00:00 2001 From: daftscience Date: Sat, 16 Mar 2013 05:30:02 -0300 Subject: [PATCH 02/22] Reset currentLine when changing state to printing This keeps gcodePos and current line in sync when changing status to "Printing" --- octoprint/util/comm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 9be720a..a1f9d7e 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -486,7 +486,10 @@ class MachineCom(object): pass commandToSend = cmd - self._currentLine += 1 + if "M110" in cmd: + pass + else: + self._currentLine += 1 if sendChecksum or self._alwaysSendChecksum: lineNumber = self._gcodePos if self._alwaysSendChecksum: @@ -558,6 +561,7 @@ class MachineCom(object): return self._gcodeList = gcodeList self._gcodePos = 0 + self._currentLine = 0 self._printStartTime100 = None self._printSection = 'CUSTOM' self._changeState(self.STATE_PRINTING) From d6a83d174fa40ab5c29acb0e8bd8222cf3c8171f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sat, 16 Mar 2013 18:25:39 +0100 Subject: [PATCH 03/22] Overhauled resend handling to also work with alwaysSendChecksum feature. Also introduced new feature flag resetLineNumbersWithPrefixedN to make M110 commands send the target line number as part of their N prefix (Repetier), not as a separate N parameter (Marlin & co) --- octoprint/printer.py | 5 +- octoprint/server.py | 7 +- octoprint/settings.py | 3 +- octoprint/static/js/ui.js | 5 +- octoprint/templates/settings.html | 7 ++ octoprint/util/comm.py | 118 +++++++++++++++++++++++------- 6 files changed, 115 insertions(+), 30 deletions(-) diff --git a/octoprint/printer.py b/octoprint/printer.py index 14edf8b..2740494 100644 --- a/octoprint/printer.py +++ b/octoprint/printer.py @@ -211,7 +211,8 @@ class Printer(): self._setProgressData(None, None, None) # mark print as failure - self._gcodeManager.printFailed(self._filename) + if self._filename is not None: + self._gcodeManager.printFailed(self._filename) #~~ state monitoring @@ -480,7 +481,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: diff --git a/octoprint/server.py b/octoprint/server.py index 193b3ee..575aa75 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -360,7 +360,8 @@ def getSettings(): "feature": { "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), - "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]) + "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]), + "resetLineNumbersWithPrefixedN": s.getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) }, "folder": { "uploads": s.getBaseFolder("uploads"), @@ -403,6 +404,7 @@ def setSettings(): 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 "folder" in data.keys(): if "uploads" in data["folder"].keys(): s.setBaseFolder("uploads", data["folder"]["uploads"]) @@ -521,6 +523,9 @@ class Server(): } }, "loggers": { + #"octoprint.util.comm": { + # "level": "DEBUG" + #}, "SERIAL": { "level": "DEBUG", "handlers": ["serialFile"], diff --git a/octoprint/settings.py b/octoprint/settings.py index 93a94d1..7418251 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -40,7 +40,8 @@ default_settings = { "feature": { "gCodeVisualizer": True, "waitForStartOnConnect": False, - "alwaysSendChecksum": False + "alwaysSendChecksum": False, + "resetLineNumbersWithPrefixedN": False }, "folder": { "uploads": None, diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 7b0942d..36dee20 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -960,6 +960,7 @@ function SettingsViewModel() { 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.folder_uploads = ko.observable(undefined); self.folder_timelapse = ko.observable(undefined); @@ -1005,6 +1006,7 @@ function SettingsViewModel() { 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.folder_uploads(response.folder.uploads); self.folder_timelapse(response.folder.timelapse); @@ -1038,7 +1040,8 @@ function SettingsViewModel() { "feature": { "gcodeViewer": self.feature_gcodeViewer(), "waitForStart": self.feature_waitForStart(), - "alwaysSendChecksum": self.feature_alwaysSendChecksum() + "alwaysSendChecksum": self.feature_alwaysSendChecksum(), + "resetLineNumbersWithPrefixedN": self.feature_resetLineNumbersWithPrefixedN() }, "folder": { "uploads": self.folder_uploads(), diff --git a/octoprint/templates/settings.html b/octoprint/templates/settings.html index 80a4840..57f6c2b 100644 --- a/octoprint/templates/settings.html +++ b/octoprint/templates/settings.html @@ -113,6 +113,13 @@
+
+
+ +
+
diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index a1f9d7e..52c81b8 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -64,6 +64,8 @@ class VirtualPrinter(): self.bedTemp = 1.0 self.bedTargetTemp = 1.0 + self.currentLine = 0 + def write(self, data): if self.readList is None: return @@ -79,9 +81,18 @@ class VirtualPrinter(): except: pass if 'M105' in data: + # 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 "M110" in data: + # reset current line + self.currentLine = int(re.search('N([0-9]+)', data).group(1)) + self.readList.append("ok\n") + elif self.currentLine == 100: + # simulate a resend at line 100 of the last 5 lines + self.readList.append("rs %d\n" % (self.currentLine - 5)) elif len(data.strip()) > 0: self.readList.append("ok\n") + self.currentLine += 1 def readline(self): if self.readList is None: @@ -180,7 +191,9 @@ class MachineCom(object): self._printStartTime100 = None self._alwaysSendChecksum = settings().getBoolean(["feature", "alwaysSendChecksum"]) - self._currentLine = 0 + self._currentLine = 1 + self._resendDelta = None + self._lastLines = [] self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True @@ -409,22 +422,34 @@ class MachineCom(object): 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 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 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() elif "resend" in line.lower() or "rs" in line: + lineToResend = None try: - self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) + lineToResend = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) except: if "rs" in line: - self._gcodePos = int(line.split()[1]) + lineToResend = int(line.split()[1]) + + if lineToResend is not None: + self._resendDelta = self._currentLine - lineToResend + if self._resendDelta > len(self._lastLines): + self._errorValue = "Printer requested line %d but history is only available up to line %d" % (lineToResend, self._currentLine - len(self._lastLines)) + self._changeState(self.STATE_ERROR) + self._logger.warn(self._errorValue) + else: + self._resendNextCommand() self._log("Connection closed, closing down monitor") def _log(self, message): @@ -467,7 +492,18 @@ class MachineCom(object): def __del__(self): self.close() - + + def _resendNextCommand(self): + self._logger.debug("Resending line %d, delta is %d, history log is %s items strong" % (self._currentLine - self._resendDelta, self._resendDelta, len(self._lastLines))) + cmd = self._lastLines[-self._resendDelta] + lineNumber = self._currentLine - self._resendDelta + + self._doSendWithChecksum(cmd, lineNumber) + + self._resendDelta -= 1 + if self._resendDelta < 0: + self._resendDelta = None + def _sendCommand(self, cmd, sendChecksum=False): cmd = cmd.upper() if self._serial is None: @@ -485,25 +521,67 @@ class MachineCom(object): except: pass - commandToSend = cmd if "M110" in cmd: - pass + newLineNumber = None + if " N" in cmd: + try: + newLineNumber = int(re.search("N([0-9]+)", cmd).group(1)) + except: + pass + else: + newLineNumber = 0 + + if settings().getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) and newLineNumber is not None: + # let's rewrite the M110 command to fit repetier syntax + self._doSendWithChecksum("M110", newLineNumber) + self._addToLastLines(cmd) + self._currentLine = newLineNumber + 1 + else: + self._doSend(cmd, sendChecksum) + if newLineNumber is not None: + self._currentLine = newLineNumber + 1 + + # after a reset of the line number we have no way to determine what line exactly the printer now wants + self._lastLines = [] + self._resendDelta = None else: - self._currentLine += 1 + self._doSend(cmd, sendChecksum) + + def _addToLastLines(self, cmd): + self._lastLines.append(cmd) + if len(self._lastLines) > 50: + self._lastLines = self._lastLines[-50:] # only keep the last 50 lines in memory + self._logger.debug("Got %d lines of history in memory" % len(self._lastLines)) + + def _doSend(self, cmd, sendChecksum=False): if sendChecksum or self._alwaysSendChecksum: - lineNumber = self._gcodePos if self._alwaysSendChecksum: lineNumber = self._currentLine - checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNumber, cmd))) - commandToSend = "N%d%s*%d" % (lineNumber, cmd, checksum) + else: + lineNumber = self._gcodePos + self._doSendWithChecksum(cmd, lineNumber) + self._addToLastLines(cmd) + self._currentLine += 1 + else: + self._doSendWithoutChecksum(cmd) - self._log('Send: %s' % (commandToSend)) + def _doSendWithChecksum(self, cmd, lineNumber=None): + self._logger.debug("Sending cmd '%s' with lineNumber %r" % (cmd, lineNumber)) + + if lineNumber is None: + lineNumber = self._currentLine + checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNumber, cmd))) + commandToSend = "N%d%s*%d" % (lineNumber, cmd, checksum) + self._doSendWithoutChecksum(commandToSend) + + def _doSendWithoutChecksum(self, cmd): + self._log("Send: %s" % cmd) try: - self._serial.write(commandToSend + '\n') + self._serial.write(cmd + '\n') except serial.SerialTimeoutException: self._log("Serial timeout while writing to serial port, trying again.") try: - self._serial.write(commandToSend + '\n') + self._serial.write(cmd + '\n') except: self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() @@ -512,15 +590,6 @@ class MachineCom(object): self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) - finally: - if "M110" in cmd: - if " N" in cmd: - try: - self._currentLine = int(re.search("N([0-9]+)", cmd).group(1)) - except: - pass - else: - self._currentLine = 0 def _sendNext(self): if self._gcodePos >= len(self._gcodeList): @@ -561,7 +630,6 @@ class MachineCom(object): return self._gcodeList = gcodeList self._gcodePos = 0 - self._currentLine = 0 self._printStartTime100 = None self._printSection = 'CUSTOM' self._changeState(self.STATE_PRINTING) From ad1cbca22a77d5745820ab234f2da3233ceaf798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 17 Mar 2013 10:58:34 +0100 Subject: [PATCH 04/22] Added known error messages for checksum mismatches or expected line issues from Repetier to recognized "auto-correction" errors, made code around all that a bit more readable. --- octoprint/util/comm.py | 43 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 52c81b8..9b16f33 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -89,10 +89,13 @@ class VirtualPrinter(): self.readList.append("ok\n") elif self.currentLine == 100: # simulate a resend at line 100 of the last 5 lines + self.readList.append("Error: Line Number is not Last Line Number\n") self.readList.append("rs %d\n" % (self.currentLine - 5)) elif len(data.strip()) > 0: self.readList.append("ok\n") - self.currentLine += 1 + + if "*" in data: + self.currentLine += 1 def readline(self): if self.readList is None: @@ -340,6 +343,8 @@ class MachineCom(object): if line == None: break + + ### Error detection #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. @@ -349,24 +354,41 @@ class MachineCom(object): if re.match('Error:[0-9]\n', line): line = line.rstrip() + self._readline() #Skip the communication errors, as those get corrected. - if 'checksum mismatch' in line or 'Line Number is not Last Line Number' in line or 'No Line Number with checksum' in line or 'No Checksum with line number' in line: + if 'checksum mismatch' in line \ + or 'Wrong checksum' in line \ + or 'Line Number is not Last Line Number' in line \ + or 'expected line' in line \ + or 'No Line Number with checksum' in line \ + or 'No Checksum with line number' in line \ + or 'Missing checksum' in line: pass elif not self.isError(): self._errorValue = line[6:] self._changeState(self.STATE_ERROR) + + ### Evaluate temperature status messages 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: - self._bedTemp = float(re.search("-?[0-9\.]*", line.split(' B:')[1]).group(0)) - self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp) + try: + self._temp = float(re.search("-?[0-9\.]*", line.split('T:')[1]).group(0)) + if ' B:' in line: + self._bedTemp = float(re.search("-?[0-9\.]*", line.split(' B:')[1]).group(0)) + + self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp) + except ValueError: + # catch conversion issues, we'll rather just not get the temperature update instead of killing the connection + pass + #If we are waiting for an M109 or M190 then measure the time we lost during heatup, so we can remove that time from our printing time estimate. if not 'ok' in line and self._heatupWaitStartTime != 0: t = time.time() self._heatupWaitTimeLost = t - self._heatupWaitStartTime self._heatupWaitStartTime = t + + ### Forward messages from the firmware elif line.strip() != '' and line.strip() != 'ok' and not line.startswith("wait") and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational(): self._callback.mcMessage(line) + ### Baudrate detection if self._state == self.STATE_DETECT_BAUDRATE: if line == '' or time.time() > timeout: if len(self._baudrateDetectList) < 1: @@ -404,6 +426,8 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) else: self._testingBaudrate = False + + ### Connection attempt elif self._state == self.STATE_CONNECTING: if (line == "" or "wait" in line) and startSeen: self._sendCommand("M105") @@ -413,11 +437,15 @@ class MachineCom(object): self._changeState(self.STATE_OPERATIONAL) elif time.time() > timeout: self.close() + + ### Operational elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: #Request the temperature on comm timeout (every 5 seconds) when we are not printing. if line == "" or "wait" in line: self._sendCommand("M105") tempRequestTimeout = time.time() + 5 + + ### Printing elif self._state == self.STATE_PRINTING: if line == "" and time.time() > timeout: self._log("Communication timeout during printing, forcing a line") @@ -426,6 +454,8 @@ class MachineCom(object): if time.time() > tempRequestTimeout: self._commandQueue.put("M105") tempRequestTimeout = time.time() + 5 + + # ok -> send next command if "ok" in line: timeout = time.time() + 5 if self._resendDelta is not None: @@ -434,6 +464,7 @@ class MachineCom(object): self._sendCommand(self._commandQueue.get()) else: self._sendNext() + # resend -> start resend procedure from requested line elif "resend" in line.lower() or "rs" in line: lineToResend = None try: From 456ded3f3609c9a49b2942b683af3303e484506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Sun, 17 Mar 2013 13:53:38 +0100 Subject: [PATCH 05/22] Fixed off by one error in resend loop --- octoprint/util/comm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 9b16f33..e5382cd 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -532,7 +532,7 @@ class MachineCom(object): self._doSendWithChecksum(cmd, lineNumber) self._resendDelta -= 1 - if self._resendDelta < 0: + if self._resendDelta <= 0: self._resendDelta = None def _sendCommand(self, cmd, sendChecksum=False): From 9b2d166c6cd0424053328c8516e6f616f1785bb9 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 25 Mar 2013 14:01:48 -0400 Subject: [PATCH 06/22] Prevents manual commands from interupting other commands --- octoprint/util/comm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index e5382cd..67cacbe 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -197,7 +197,7 @@ class MachineCom(object): self._currentLine = 1 self._resendDelta = None self._lastLines = [] - + self._sending = False self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() @@ -537,6 +537,10 @@ class MachineCom(object): def _sendCommand(self, cmd, sendChecksum=False): cmd = cmd.upper() + #Wait for current send to finish. + while self._sending: + pass + self._sending = True if self._serial is None: return if 'M109' in cmd or 'M190' in cmd: @@ -621,6 +625,8 @@ class MachineCom(object): self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) + #clear sending flag + self._sending = False def _sendNext(self): if self._gcodePos >= len(self._gcodeList): From 48a2fd71a7ba94cf6babe9be9e9ec3149ba1c4b7 Mon Sep 17 00:00:00 2001 From: daftscience Date: Tue, 26 Mar 2013 07:25:16 +0000 Subject: [PATCH 07/22] More reliable initialization of communication with repetier --- octoprint/util/comm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 67cacbe..8bad3b6 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -429,7 +429,9 @@ class MachineCom(object): ### Connection attempt elif self._state == self.STATE_CONNECTING: - if (line == "" or "wait" in line) and startSeen: + #if (line == "" or "wait" in line) and startSeen: + #This modification allows more reliable initial connection. + if ("wait" in line) and startSeen: self._sendCommand("M105") elif "start" in line: startSeen = True From f050567a1c246d184dcfec897323bb5a627c3908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 29 Mar 2013 22:17:44 +0100 Subject: [PATCH 08/22] User thread lock instead of boolean to ensure only one concurrent sending thread, introduced wait flag for repetier printers --- octoprint/server.py | 2 + octoprint/settings.py | 1 + octoprint/static/js/ui.js | 4 + octoprint/templates/settings.html | 13 ++- octoprint/util/comm.py | 162 +++++++++++++++++------------- 5 files changed, 107 insertions(+), 75 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index 575aa75..1025f58 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -360,6 +360,7 @@ def getSettings(): "feature": { "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), + "waitForWaitAfterStart": s.getBoolean(["feature", "waitForWaitAfterStartOnConnect"]), "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]), "resetLineNumbersWithPrefixedN": s.getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) }, @@ -403,6 +404,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 "waitForWait" in data["feature"].keys(): s.setBoolean(["feature", "waitForWaitOnConnect"], data["feature"]["waitForWait"]), 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"]) diff --git a/octoprint/settings.py b/octoprint/settings.py index 7418251..10e99b4 100644 --- a/octoprint/settings.py +++ b/octoprint/settings.py @@ -40,6 +40,7 @@ default_settings = { "feature": { "gCodeVisualizer": True, "waitForStartOnConnect": False, + "waitForWaitOnConnect": False, "alwaysSendChecksum": False, "resetLineNumbersWithPrefixedN": False }, diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 36dee20..1cb4364 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -618,6 +618,7 @@ function TerminalViewModel() { if (!self.log) self.log = [] self.log = self.log.concat(data) + self.log = self.log.slice(-300) self.updateOutput(); } @@ -959,6 +960,7 @@ function SettingsViewModel() { self.feature_gcodeViewer = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined); + self.feature_waitForWait = ko.observable(undefined); self.feature_alwaysSendChecksum = ko.observable(undefined); self.feature_resetLineNumbersWithPrefixedN = ko.observable(undefined); @@ -1005,6 +1007,7 @@ function SettingsViewModel() { self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_waitForStart(response.feature.waitForStart); + self.feature_waitForWait(response.feature.waitForWait); self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum); self.feature_resetLineNumbersWithPrefixedN(response.feature.resetLineNumbersWithPrefixedN); @@ -1040,6 +1043,7 @@ function SettingsViewModel() { "feature": { "gcodeViewer": self.feature_gcodeViewer(), "waitForStart": self.feature_waitForStart(), + "waitForWait": self.feature_waitForWait(), "alwaysSendChecksum": self.feature_alwaysSendChecksum(), "resetLineNumbersWithPrefixedN": self.feature_resetLineNumbersWithPrefixedN() }, diff --git a/octoprint/templates/settings.html b/octoprint/templates/settings.html index 57f6c2b..6d8af47 100644 --- a/octoprint/templates/settings.html +++ b/octoprint/templates/settings.html @@ -102,21 +102,28 @@
+
+
+
+
+
diff --git a/octoprint/util/comm.py b/octoprint/util/comm.py index 8bad3b6..c6b8a0a 100644 --- a/octoprint/util/comm.py +++ b/octoprint/util/comm.py @@ -66,6 +66,9 @@ class VirtualPrinter(): self.currentLine = 0 + waitThread = threading.Thread(target=self._sendWaitAfterTimeout) + waitThread.start() + def write(self, data): if self.readList is None: return @@ -125,6 +128,10 @@ class VirtualPrinter(): def close(self): self.readList = None + def _sendWaitAfterTimeout(self, timeout=5): + time.sleep(timeout) + self.readList.append("wait") + class MachineComPrintCallback(object): def mcLog(self, message): pass @@ -197,7 +204,9 @@ class MachineCom(object): self._currentLine = 1 self._resendDelta = None self._lastLines = [] - self._sending = False + + self._sendingLock = threading.Lock() + self.thread = threading.Thread(target=self._monitor) self.thread.daemon = True self.thread.start() @@ -338,6 +347,7 @@ class MachineCom(object): timeout = time.time() + 5 tempRequestTimeout = timeout startSeen = not settings().getBoolean(["feature", "waitForStartOnConnect"]) + waitSeen = not settings().getBoolean(["feature", "waitForWaitOnConnect"]) while True: line = self._readline() if line == None: @@ -429,13 +439,13 @@ class MachineCom(object): ### Connection attempt elif self._state == self.STATE_CONNECTING: - #if (line == "" or "wait" in line) and startSeen: - #This modification allows more reliable initial connection. - if ("wait" in line) and startSeen: + if line == "" and startSeen and waitSeen: self._sendCommand("M105") elif "start" in line: startSeen = True - elif "ok" in line and startSeen: + elif "wait" in line: + waitSeen = True + elif "ok" in line and startSeen and waitSeen: self._changeState(self.STATE_OPERATIONAL) elif time.time() > timeout: self.close() @@ -444,8 +454,16 @@ class MachineCom(object): elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED: #Request the temperature on comm timeout (every 5 seconds) when we are not printing. if line == "" or "wait" in line: - self._sendCommand("M105") + if self._resendDelta is not None: + self._resendNextCommand() + elif not self._commandQueue.empty(): + self._sendCommand(self._commandQueue.get()) + else: + self._sendCommand("M105") tempRequestTimeout = time.time() + 5 + # resend -> start resend procedure from requested line + elif "resend" in line.lower() or "rs" in line: + self._handleResendRequest(line) ### Printing elif self._state == self.STATE_PRINTING: @@ -468,23 +486,26 @@ class MachineCom(object): self._sendNext() # resend -> start resend procedure from requested line elif "resend" in line.lower() or "rs" in line: - lineToResend = None - try: - lineToResend = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) - except: - if "rs" in line: - lineToResend = int(line.split()[1]) - - if lineToResend is not None: - self._resendDelta = self._currentLine - lineToResend - if self._resendDelta > len(self._lastLines): - self._errorValue = "Printer requested line %d but history is only available up to line %d" % (lineToResend, self._currentLine - len(self._lastLines)) - self._changeState(self.STATE_ERROR) - self._logger.warn(self._errorValue) - else: - self._resendNextCommand() + self._handleResendRequest(line) self._log("Connection closed, closing down monitor") - + + def _handleResendRequest(self, line): + lineToResend = None + try: + lineToResend = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) + except: + if "rs" in line: + lineToResend = int(line.split()[1]) + + if lineToResend is not None: + self._resendDelta = self._currentLine - lineToResend + if self._resendDelta > len(self._lastLines): + self._errorValue = "Printer requested line %d but history is only available up to line %d" % (lineToResend, self._currentLine - len(self._lastLines)) + self._changeState(self.STATE_ERROR) + self._logger.warn(self._errorValue) + else: + self._resendNextCommand() + def _log(self, message): self._callback.mcLog(message) self._serialLogger.debug(message) @@ -527,62 +548,63 @@ class MachineCom(object): self.close() def _resendNextCommand(self): - self._logger.debug("Resending line %d, delta is %d, history log is %s items strong" % (self._currentLine - self._resendDelta, self._resendDelta, len(self._lastLines))) - cmd = self._lastLines[-self._resendDelta] - lineNumber = self._currentLine - self._resendDelta + # Make sure we are only handling one sending job at a time + with self._sendingLock: + self._logger.debug("Resending line %d, delta is %d, history log is %s items strong" % (self._currentLine - self._resendDelta, self._resendDelta, len(self._lastLines))) + cmd = self._lastLines[-self._resendDelta] + lineNumber = self._currentLine - self._resendDelta - self._doSendWithChecksum(cmd, lineNumber) + self._doSendWithChecksum(cmd, lineNumber) - self._resendDelta -= 1 - if self._resendDelta <= 0: - self._resendDelta = None + self._resendDelta -= 1 + if self._resendDelta <= 0: + self._resendDelta = None def _sendCommand(self, cmd, sendChecksum=False): - cmd = cmd.upper() - #Wait for current send to finish. - while self._sending: - pass - self._sending = True - if self._serial is None: - return - if 'M109' in cmd or 'M190' in cmd: - self._heatupWaitStartTime = time.time() - if 'M104' in cmd or 'M109' in cmd: - try: - self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1)) - except: - pass - if 'M140' in cmd or 'M190' in cmd: - try: - self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1)) - except: - pass + # Make sure we are only handling one sending job at a time + with self._sendingLock: + cmd = cmd.upper() - if "M110" in cmd: - newLineNumber = None - if " N" in cmd: + if self._serial is None: + return + if 'M109' in cmd or 'M190' in cmd: + self._heatupWaitStartTime = time.time() + if 'M104' in cmd or 'M109' in cmd: try: - newLineNumber = int(re.search("N([0-9]+)", cmd).group(1)) + self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1)) + except: + pass + if 'M140' in cmd or 'M190' in cmd: + try: + self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1)) except: pass - else: - newLineNumber = 0 - if settings().getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) and newLineNumber is not None: - # let's rewrite the M110 command to fit repetier syntax - self._doSendWithChecksum("M110", newLineNumber) - self._addToLastLines(cmd) - self._currentLine = newLineNumber + 1 - else: - self._doSend(cmd, sendChecksum) + if "M110" in cmd: + newLineNumber = None + if " N" in cmd: + try: + newLineNumber = int(re.search("N([0-9]+)", cmd).group(1)) + except: + pass + else: + newLineNumber = 0 + + if settings().getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) and newLineNumber is not None: + # let's rewrite the M110 command to fit repetier syntax + self._addToLastLines(cmd) + self._doSendWithChecksum("M110", newLineNumber) + else: + self._doSend(cmd, sendChecksum) + if newLineNumber is not None: self._currentLine = newLineNumber + 1 - # after a reset of the line number we have no way to determine what line exactly the printer now wants - self._lastLines = [] - self._resendDelta = None - else: - self._doSend(cmd, sendChecksum) + # after a reset of the line number we have no way to determine what line exactly the printer now wants + self._lastLines = [] + self._resendDelta = None + else: + self._doSend(cmd, sendChecksum) def _addToLastLines(self, cmd): self._lastLines.append(cmd) @@ -596,17 +618,15 @@ class MachineCom(object): lineNumber = self._currentLine else: lineNumber = self._gcodePos - self._doSendWithChecksum(cmd, lineNumber) self._addToLastLines(cmd) self._currentLine += 1 + self._doSendWithChecksum(cmd, lineNumber) else: self._doSendWithoutChecksum(cmd) - def _doSendWithChecksum(self, cmd, lineNumber=None): + def _doSendWithChecksum(self, cmd, lineNumber): self._logger.debug("Sending cmd '%s' with lineNumber %r" % (cmd, lineNumber)) - if lineNumber is None: - lineNumber = self._currentLine checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (lineNumber, cmd))) commandToSend = "N%d%s*%d" % (lineNumber, cmd, checksum) self._doSendWithoutChecksum(commandToSend) @@ -627,8 +647,6 @@ class MachineCom(object): self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) self._errorValue = getExceptionString() self.close(True) - #clear sending flag - self._sending = False def _sendNext(self): if self._gcodePos >= len(self._gcodeList): From e70071f6c113412bae5294982c1c3366647548f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 1 Apr 2013 17:31:02 +0200 Subject: [PATCH 09/22] Removed waitForWait again (that didn't make much sense to begin with...) --- octoprint/server.py | 2 -- octoprint/static/js/ui.js | 3 --- octoprint/templates/settings.html | 7 ------- octoprint/util/comm.py | 7 ++----- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/octoprint/server.py b/octoprint/server.py index a625261..f620266 100644 --- a/octoprint/server.py +++ b/octoprint/server.py @@ -363,7 +363,6 @@ def getSettings(): "feature": { "gcodeViewer": s.getBoolean(["feature", "gCodeVisualizer"]), "waitForStart": s.getBoolean(["feature", "waitForStartOnConnect"]), - "waitForWaitAfterStart": s.getBoolean(["feature", "waitForWaitAfterStartOnConnect"]), "alwaysSendChecksum": s.getBoolean(["feature", "alwaysSendChecksum"]), "resetLineNumbersWithPrefixedN": s.getBoolean(["feature", "resetLineNumbersWithPrefixedN"]) }, @@ -407,7 +406,6 @@ 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 "waitForWait" in data["feature"].keys(): s.setBoolean(["feature", "waitForWaitOnConnect"], data["feature"]["waitForWait"]), 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"]) diff --git a/octoprint/static/js/ui.js b/octoprint/static/js/ui.js index 240dfe3..222df2b 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -994,7 +994,6 @@ function SettingsViewModel() { self.feature_gcodeViewer = ko.observable(undefined); self.feature_waitForStart = ko.observable(undefined); - self.feature_waitForWait = ko.observable(undefined); self.feature_alwaysSendChecksum = ko.observable(undefined); self.feature_resetLineNumbersWithPrefixedN = ko.observable(undefined); @@ -1041,7 +1040,6 @@ function SettingsViewModel() { self.feature_gcodeViewer(response.feature.gcodeViewer); self.feature_waitForStart(response.feature.waitForStart); - self.feature_waitForWait(response.feature.waitForWait); self.feature_alwaysSendChecksum(response.feature.alwaysSendChecksum); self.feature_resetLineNumbersWithPrefixedN(response.feature.resetLineNumbersWithPrefixedN); @@ -1077,7 +1075,6 @@ function SettingsViewModel() { "feature": { "gcodeViewer": self.feature_gcodeViewer(), "waitForStart": self.feature_waitForStart(), - "waitForWait": self.feature_waitForWait(), "alwaysSendChecksum": self.feature_alwaysSendChecksum(), "resetLineNumbersWithPrefixedN": self.feature_resetLineNumbersWithPrefixedN() }, diff --git a/octoprint/templates/settings.html b/octoprint/templates/settings.html index 6d8af47..f9bc048 100644 --- a/octoprint/templates/settings.html +++ b/octoprint/templates/settings.html @@ -106,13 +106,6 @@
-
-
- -
-
+
+
+ +
+
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]) From b048cc390b5f000771f189d0a2ab577a69ce409a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 20 May 2013 23:31:17 +0200 Subject: [PATCH 11/22] Proper uploading incl. drag-n-drop for both local storage and SD card --- octoprint/static/css/octoprint.less | 127 +++++++++++++++++++++++++++ octoprint/static/js/ui.js | 129 +++++++++++++++++++++------- octoprint/templates/dialogs.jinja2 | 15 ++++ octoprint/templates/index.jinja2 | 30 ++++--- 4 files changed, 258 insertions(+), 43 deletions(-) diff --git a/octoprint/static/css/octoprint.less b/octoprint/static/css/octoprint.less index 2f8c258..29c0bb5 100644 --- a/octoprint/static/css/octoprint.less +++ b/octoprint/static/css/octoprint.less @@ -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 cd794d4..3765ee9 100644 --- a/octoprint/static/js/ui.js +++ b/octoprint/static/js/ui.js @@ -1885,43 +1885,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 }); - $("#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 ..."); - } + 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 @@ -2007,10 +2067,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 %} +
+
+