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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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 @@
-
-
- -
-