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] 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)