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.

master
daid 2012-09-06 16:52:05 +02:00
parent 1f7924688d
commit 9c47af5797
2 changed files with 293 additions and 156 deletions

View File

@ -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):

View File

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