From 9c47af579716f16e8aa0bcf2d15ba5f11a37531d Mon Sep 17 00:00:00 2001 From: daid Date: Thu, 6 Sep 2012 16:52:05 +0200 Subject: [PATCH] Major update on machine communication. Part 1, this breaks realtime speed tuning, pausing, M0/M1, height reporting and timelaps recording. But it is worth it. --- Cura/gui/printWindow.py | 167 +++++++++--------------- Cura/util/machineCom.py | 282 +++++++++++++++++++++++++++++++++------- 2 files changed, 293 insertions(+), 156 deletions(-) diff --git a/Cura/gui/printWindow.py b/Cura/gui/printWindow.py index 6eaeb3ea..70e373a0 100644 --- a/Cura/gui/printWindow.py +++ b/Cura/gui/printWindow.py @@ -68,11 +68,13 @@ class PrintCommandButton(buttons.GenBitmapButton): self.Bind(wx.EVT_BUTTON, self.OnClick) def OnClick(self, e): - if self.parent.printIdx != None: + if self.parent.machineCom == None or self.parent.machineCom.isPrinting(): return; - self.parent.sendCommand("G91") - self.parent.sendCommand(self.command) - self.parent.sendCommand("G90") + if self.command.startswith('G1'): + self.parent.machineCom.sendCommand("G91") + self.parent.machineCom.sendCommand(self.command) + if self.command.startswith('G1'): + self.parent.machineCom.sendCommand("G90") e.Skip() class printWindow(wx.Frame): @@ -80,12 +82,9 @@ class printWindow(wx.Frame): def __init__(self): super(printWindow, self).__init__(None, -1, title='Printing') self.machineCom = None - self.machineConnected = False - self.thread = None self.gcode = None self.gcodeList = None self.sendList = [] - self.printIdx = None self.temp = None self.bedTemp = None self.bufferLineCount = 4 @@ -114,7 +113,7 @@ class printWindow(wx.Frame): sb = wx.StaticBox(self.panel, label="Statistics") boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL) - self.statsText = wx.StaticText(self.panel, -1, "Filament: ####.##m #.##g\nPrint time: #####:##") + self.statsText = wx.StaticText(self.panel, -1, "Filament: ####.##m #.##g\nPrint time: #####:##\nMachine state: Detecting baudrate") boxsizer.Add(self.statsText, flag=wx.LEFT, border=5) self.sizer.Add(boxsizer, pos=(0,0), span=(5,1), flag=wx.EXPAND) @@ -276,10 +275,10 @@ class printWindow(wx.Frame): self.Centre() self.UpdateButtonStates() - self.UpdateProgress() + #self.UpdateProgress() def OnCameraTimer(self, e): - if self.printIdx != None: + if self.machineCom != None and not self.machineCom.isPrinting(): return self.cam.takeNewImage() self.camPage.Refresh() @@ -296,14 +295,14 @@ class printWindow(wx.Frame): dc.DrawBitmap(self.cam.getLastImage(), 0, 0) def UpdateButtonStates(self): - self.connectButton.Enable(not self.machineConnected) + self.connectButton.Enable(self.machineCom == None or self.machineCom.isClosedOrError()) #self.loadButton.Enable(self.printIdx == None) - self.printButton.Enable(self.machineConnected and self.gcodeList != None and self.printIdx == None) - self.pauseButton.Enable(self.printIdx != None) - self.cancelButton.Enable(self.printIdx != None) - self.temperatureSelect.Enable(self.machineConnected) - self.bedTemperatureSelect.Enable(self.machineConnected) - self.directControlPanel.Enable(self.machineConnected) + self.printButton.Enable(self.machineCom != None and self.machineCom.isOperational() and not self.machineCom.isPrinting()) + self.pauseButton.Enable(self.machineCom != None and self.machineCom.isPrinting()) + self.cancelButton.Enable(self.machineCom != None and self.machineCom.isPrinting()) + self.temperatureSelect.Enable(self.machineCom != None and self.machineCom.isOperational()) + self.bedTemperatureSelect.Enable(self.machineCom != None and self.machineCom.isOperational()) + self.directControlPanel.Enable(self.machineCom != None and self.machineCom.isOperational()) def UpdateProgress(self): status = "" @@ -315,68 +314,62 @@ class printWindow(wx.Frame): if cost != False: status += "Filament cost: %s\n" % (cost) status += "Print time: %02d:%02d\n" % (int(self.gcode.totalMoveTimeMinute / 60), int(self.gcode.totalMoveTimeMinute % 60)) - if self.printIdx == None: + if self.machineCom == None or not self.machineCom.isPrinting(): self.progress.SetValue(0) if self.gcodeList != None: status += 'Line: -/%d\n' % (len(self.gcodeList)) else: - status += 'Line: %d/%d\n' % (self.printIdx, len(self.gcodeList)) + status += 'Line: %d/%d\n' % (self.machineCom.getPrintPos(), len(self.gcodeList)) status += 'Height: %f\n' % (self.currentZ) - self.progress.SetValue(self.printIdx) - if self.temp != None: - status += 'Temp: %d\n' % (self.temp) - if self.bedTemp != None and self.bedTemp > 0: - status += 'Bed Temp: %d\n' % (self.bedTemp) - self.bedTemperatureLabel.Show(True) - self.bedTemperatureSelect.Show(True) - self.temperaturePanel.Layout() + self.progress.SetValue(self.machineCom.getPrintPos()) + if self.machineCom != None: + if self.machineCom.getTemp() > 0: + status += 'Temp: %d\n' % (self.machineCom.getTemp()) + if self.machineCom.getBedTemp() > 0: + status += 'Bed Temp: %d\n' % (self.machineCom.getBedTemp()) + self.bedTemperatureLabel.Show(True) + self.bedTemperatureSelect.Show(True) + self.temperaturePanel.Layout() + status += 'Machine state: %s\n' % (self.machineCom.getStateString()) + self.statsText.SetLabel(status.strip()) - #self.Layout() def OnConnect(self, e): if self.machineCom != None: self.machineCom.close() - self.thread.join() - self.machineCom = machineCom.MachineCom() - self.thread = threading.Thread(target=self.PrinterMonitor) - self.thread.start() + self.machineCom = machineCom.MachineCom(callbackObject=self) self.UpdateButtonStates() def OnLoad(self, e): pass def OnPrint(self, e): - if not self.machineConnected: + if self.machineCom == None or not self.machineCom.isOperational(): return if self.gcodeList == None: return - if self.printIdx != None: + if self.machineCom.isPrinting(): return self.currentZ = -1 if self.cam != None: self.cam.startTimelaps(self.filename[: self.filename.rfind('.')] + ".mpg") - self.printIdx = 1 - self.sendLine(0) - self.sendCnt = self.bufferLineCount + self.machineCom.printGCode(self.gcodeList) self.UpdateButtonStates() def OnCancel(self, e): if self.cam != None: self.cam.endTimelaps() - self.printIdx = None - self.pause = False self.pauseButton.SetLabel('Pause') - self.sendCommand("M84") + self.machineCom.cancelPrint() + self.machineCom.sendCommand("M84") self.UpdateButtonStates() def OnPause(self, e): - if self.pause: - self.pause = False - self.sendLine(self.printIdx) - self.printIdx += 1 + if self.machineCom.isPaused(): + self.machineCom.setPause(False) self.pauseButton.SetLabel('Pause') else: - self.pause = True + self.machineCom.setPause(True) self.pauseButton.SetLabel('Resume') def OnClose(self, e): @@ -384,14 +377,13 @@ class printWindow(wx.Frame): printWindowHandle = None if self.machineCom != None: self.machineCom.close() - self.thread.join() self.Destroy() def OnTempChange(self, e): - self.sendCommand("M104 S%d" % (self.temperatureSelect.GetValue())) + self.machineCom.sendCommand("M104 S%d" % (self.temperatureSelect.GetValue())) def OnBedTempChange(self, e): - self.sendCommand("M140 S%d" % (self.bedTemperatureSelect.GetValue())) + self.machineCom.sendCommand("M140 S%d" % (self.bedTemperatureSelect.GetValue())) def OnSpeedChange(self, e): self.feedrateRatioOuterWall = self.outerWallSpeedSelect.GetValue() / 100.0 @@ -407,7 +399,7 @@ class printWindow(wx.Frame): if line == '': return self.termLog.AppendText('>%s\n' % (line)) - self.sendCommand(line) + self.machineCom.sendCommand(line) self.termHistory.append(line) self.termHistoryIdx = len(self.termHistory) self.termInput.SetValue('') @@ -427,7 +419,7 @@ class printWindow(wx.Frame): e.Skip() def LoadGCodeFile(self, filename): - if self.printIdx != None: + if self.machineCom != None and self.machineCom.isPrinting(): return #Send an initial M110 to reset the line counter to zero. lineType = 'CUSTOM' @@ -454,13 +446,6 @@ class printWindow(wx.Frame): wx.CallAfter(self.UpdateButtonStates) wx.CallAfter(self.UpdateProgress) - def sendCommand(self, cmd): - if self.machineConnected: - if self.printIdx == None or self.pause: - self.machineCom.sendCommand(cmd) - else: - self.sendList.append(cmd) - def sendLine(self, lineNr): if lineNr >= len(self.gcodeList): return False @@ -495,58 +480,22 @@ class printWindow(wx.Frame): self.machineCom.sendCommand("N%d%s*%d" % (lineNr, line, checksum)) return True - def PrinterMonitor(self): - while True: - line = self.machineCom.readline() - if line == None: - self.machineConnected = False - wx.CallAfter(self.UpdateButtonStates) - return - if line == '': #When we have a communication "timeout" and we're not sending gcode, then read the temperature. - if self.printIdx == None or self.pause: - self.machineCom.sendCommand("M105") - else: - wx.CallAfter(self.AddTermLog, '!!Comm timeout, forcing next line!!\n') - line = 'ok' - if self.machineConnected: - while self.sendCnt > 0 and not self.pause: - self.sendLine(self.printIdx) - self.printIdx += 1 - self.sendCnt -= 1 - if line.startswith("start"): - self.machineConnected = True - wx.CallAfter(self.UpdateButtonStates) - elif 'T:' in line: - 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.temperatureGraph.addPoint(self.temp, self.temperatureSelect.GetValue(), self.bedTemp, self.bedTemperatureSelect.GetValue()) - wx.CallAfter(self.UpdateProgress) - elif line.strip() != 'ok': - wx.CallAfter(self.AddTermLog, line) - if self.printIdx != None: - if line.startswith("ok"): - if len(self.sendList) > 0: - self.machineCom.sendCommand(self.sendList.pop(0)) - elif self.pause: - self.sendCnt += 1 - else: - if self.sendLine(self.printIdx): - self.printIdx += 1 - else: - if self.cam != None: - self.cam.endTimelaps() - self.printIdx = None - wx.CallAfter(self.UpdateButtonStates) - wx.CallAfter(self.UpdateProgress) - elif "resend" in line.lower() or "rs" in line: - try: - lineNr=int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1]) - except: - if "rs" in line: - lineNr=int(line.split()[1]) - self.printIdx = lineNr - #we should actually resend the line here, but we also get an "ok" for each error from Marlin. And thus we'll resend on the OK. + def mcLog(self, message): + #print message + pass + + def mcTempUpdate(self, temp, bedTemp): + self.temperatureGraph.addPoint(temp, self.temperatureSelect.GetValue(), bedTemp, self.bedTemperatureSelect.GetValue()) + + def mcStateChange(self, state): + wx.CallAfter(self.UpdateButtonStates) + wx.CallAfter(self.UpdateProgress) + + def mcMessage(self, message): + wx.CallAfter(self.AddTermLog, message) + + def mcProgress(self, lineNr): + wx.CallAfter(self.UpdateProgress) class temperatureGraph(wx.Panel): def __init__(self, parent): diff --git a/Cura/util/machineCom.py b/Cura/util/machineCom.py index 96143d34..dd19231c 100644 --- a/Cura/util/machineCom.py +++ b/Cura/util/machineCom.py @@ -1,7 +1,8 @@ from __future__ import absolute_import import __init__ -import os, glob, sys, time, math, traceback +import os, glob, sys, time, math, re, traceback, threading +import Queue as queue from serial import Serial @@ -84,9 +85,33 @@ class VirtualPrinter(): def close(self): self.readList = None -class MachineCom(): - def __init__(self, port = None, baudrate = None, logCallback = None): - self._logCallback = logCallback +class MachineComPrintCallback(object): + def mcLog(self, message): + print(message) + + def mcTempUpdate(self, temp, bedTemp): + pass + + def mcStateChange(self, state): + pass + + def mcMessage(self, message): + pass + + def mcProgress(self, lineNr): + pass + +class MachineCom(object): + STATE_NONE = 0 + STATE_DETECT_BAUDRATE = 1 + STATE_CONNECTING = 2 + STATE_OPERATIONAL = 3 + STATE_PRINTING = 4 + STATE_CLOSED = 5 + STATE_ERROR = 6 + STATE_CLOSED_WITH_ERROR = 7 + + def __init__(self, port = None, baudrate = None, callbackObject = None): if port == None: port = profile.getPreference('serial_port') if baudrate == None: @@ -94,7 +119,21 @@ class MachineCom(): baudrate = 0 else: baudrate = int(profile.getPreference('serial_baud')) + if callbackObject == None: + callbackObject = MachineComPrintCallback() + + self._callback = callbackObject + self._state = self.STATE_NONE self._serial = None + self._baudrateDetectList = baudrateList() + self._baudrateDetectRetry = 0 + self._temp = 0 + self._bedTemp = 0 + self._gcodeList = None + self._gcodePos = 0 + self._commandQueue = queue.Queue() + self._logQueue = queue.Queue(256) + if port == 'AUTO': programmer = stk500v2.Stk500v2() self._log("Serial port list: %s" % (str(serialList()))) @@ -103,83 +142,200 @@ class MachineCom(): self._log("Connecting to: %s" % (port)) programmer.connect(port) self._serial = programmer.leaveISP() - self._configureSerialWithBaudrate(baudrate) break except ispBase.IspError as (e): self._log("Error while connecting to %s: %s" % (port, str(e))) pass except: self._log("Unexpected error while connecting to serial port: %s %s" % (port, getExceptionString())) - programmer.close() + programmer.close() elif port == 'VIRTUAL': self._serial = VirtualPrinter() else: try: - self._serial = Serial(port, 115200, timeout=2) - self._configureSerialWithBaudrate(baudrate) + self._log("Connecting to: %s" % (port)) + if baudrate == 0: + self._serial = Serial(port, 115200, timeout=0.1) + else: + self._serial = Serial(port, baudrate, timeout=2) except: self._log("Unexpected error while connecting to serial port: %s %s" % (port, getExceptionString())) - print - print self._serial + self._log("Connected to: %s, starting monitor" % (self._serial)) + if baudrate == 0: + self._changeState(self.STATE_DETECT_BAUDRATE) + else: + self._changeState(self.STATE_CONNECTING) + self.thread = threading.Thread(target=self._monitor) + self.thread.daemon = True + self.thread.start() - def _configureSerialWithBaudrate(self, baudrate): - if baudrate != 0: - self._serial.baudrate = baudrate + def _changeState(self, newState): + if self._state == newState: return - for baudrate in baudrateList(): - try: - self._serial.baudrate = baudrate - self._log("Trying baudrate: %d" % (baudrate)) - except: - self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString())) - continue - time.sleep(0.5) - starttime = time.time() - self.sendCommand("\nM105") - for line in self._serial: - self._log("Recv: %s" % (unicode(line, 'utf-8', 'replace').rstrip())) - if 'start' in line: - return + oldState = self.getStateString() + self._state = newState + self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) + self._callback.mcStateChange(newState) + + def getState(self): + return self._state + + def getStateString(self): + if self._state == self.STATE_NONE: + return "Offline" + if self._state == self.STATE_DETECT_BAUDRATE: + return "Detect baudrate" + if self._state == self.STATE_CONNECTING: + return "Connecting" + if self._state == self.STATE_OPERATIONAL: + return "Operational" + if self._state == self.STATE_PRINTING: + return "Printing" + if self._state == self.STATE_CLOSED: + return "Closed" + if self._state == self.STATE_ERROR: + return "Error: %s" % (self._errorValue) + if self._state == self.STATE_CLOSED_WITH_ERROR: + return "Error: %s" % (self._errorValue) + return "?%d?" % (self._state) + + def isClosedOrError(self): + return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED + + def isOperational(self): + return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING + + def isPrinting(self): + return self._state == self.STATE_PRINTING + + def getPrintPos(self): + return self._gcodePos + + def isPaused(self): + return False + + def getTemp(self): + return self._temp + + def getBedTemp(self): + return self._bedTemp + + def _monitor(self): + self._timeoutTime = time.time() + 5 + 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. + if line.startswith('Error: '): + #Oh YEAH, consistency. + # Marlin reports an MIN/MAX temp error as "Error: x\n: Extruder switched off. MAXTEMP triggered !\n" + # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" + # So we can have an extra newline in the most common case. Awesome work people. + if re.match('Error: [0-9]\n', line): + line = line.rstrip() + self._readline() + self._errorValue = line + self._changeState(self.STATE_ERROR) + if 'T:' in line: + 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) + elif line.strip() != 'ok': + self._callback.mcMessage(line) + + if self._state == self.STATE_DETECT_BAUDRATE: + if line == '' or time.time() > self._timeoutTime: + if len(self._baudrateDetectList) < 1: + self._log("No more baudrates to test, and no suitable baudrate found.") + self.close() + elif self._baudrateDetectRetry > 0: + self._baudrateDetectRetry -= 1 + self._serial.write('\n') + self._sendCommand("M105") + else: + baudrate = self._baudrateDetectList.pop(0) + try: + self._serial.baudrate = baudrate + self._serial.timeout = 0.5 + self._log("Trying baudrate: %d" % (baudrate)) + self._baudrateDetectRetry = 5 + self._timeoutTime = time.time() + 5 + self._serial.write('\n') + self._sendCommand("M105") + except: + self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString())) + elif 'ok' in line: + self._serial.timeout = 2 + self._changeState(self.STATE_OPERATIONAL) + elif self._state == self.STATE_CONNECTING: + if line == '': + self._sendCommand("M105") + elif 'ok' in line: + self._changeState(self.STATE_OPERATIONAL) + if time.time() > self._timeoutTime: + self.close() + elif self._state == self.STATE_OPERATIONAL: + #Request the temperature on comm timeout (every 2 seconds) when we are not printing. + if line == '': + self._sendCommand("M105") + elif self._state == self.STATE_PRINTING: + if line == '' and time.time() > self._timeoutTime: + self._log("Communication timeout during printing, forcing a line") + line = 'ok' if 'ok' in line: - return - #Timeout in case we get a lot of crap data from some random device. - if starttime - time.time() > 5: - break - self._serial.close() - self._serial = None + self._timeoutTime = 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): - if self._logCallback != None: - self._logCallback(message) - else: - print(message) + self._callback.mcLog(message) + try: + self._logQueue.put(message, False) + except: + #If the log queue is full, remove the first message and append the new message again + self._logQueue.get() + self._logQueue.put(message, False) - def readline(self): + def _readline(self): if self._serial == None: return None try: ret = self._serial.readline() except: self._log("Unexpected error while reading serial port: %s" % (getExceptionString())) + self._errorValue = getExceptionString() + self.close(True) + return None + if ret == '': + #self._log("Recv: TIMEOUT") return '' - if ret != '': - self._log("Recv: %s" % (unicode(ret, 'utf-8', 'replace').rstrip())) - else: - self._log("Recv: TIMEOUT") + self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').rstrip())) return ret - def close(self): + def close(self, isError = False): if self._serial != None: self._serial.close() + if isError: + self._changeState(self.STATE_CLOSED_WITH_ERROR) + else: + self._changeState(self.STATE_CLOSED) self._serial = None def __del__(self): self.close() - def isOpen(self): - return self._serial != None - - def sendCommand(self, cmd): + def _sendCommand(self, cmd): if self._serial == None: return self._log('Send: %s' % (cmd)) @@ -188,6 +344,38 @@ class MachineCom(): self._serial.write('\n') except: self._log("Unexpected error while writing serial port: %s" % (getExceptionString())) + self._errorValue = getExceptionString() + self.close(True) + + def _sendNext(self): + if self._gcodePos >= len(self._gcodeList): + self._changeState(self.STATE_OPERATIONAL) + return + line = self._gcodeList[self._gcodePos] + 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) + + def sendCommand(self, cmd): + cmd = cmd.encode('ascii', 'replace') + if self.isPrinting(): + self._commandQueue.put(cmd) + elif self.isOperational(): + self._sendCommand(cmd) + + def printGCode(self, gcodeList): + if not self.isOperational() or self.isPrinting(): + return + self._gcodeList = gcodeList + self._gcodePos = 0 + self._changeState(self.STATE_PRINTING) + for i in xrange(0, 6): + self._sendNext() + + def cancelPrint(self): + if self.isOperational(): + self._changeState(self.STATE_OPERATIONAL) def getExceptionString(): locationInfo = traceback.extract_tb(sys.exc_info()[2])[0] -- 2.30.2