self.Bind(wx.EVT_BUTTON, self.OnClick)\r
\r
def OnClick(self, e):\r
- if self.parent.printIdx != None:\r
+ if self.parent.machineCom == None or self.parent.machineCom.isPrinting():\r
return;\r
- self.parent.sendCommand("G91")\r
- self.parent.sendCommand(self.command)\r
- self.parent.sendCommand("G90")\r
+ if self.command.startswith('G1'):\r
+ self.parent.machineCom.sendCommand("G91")\r
+ self.parent.machineCom.sendCommand(self.command)\r
+ if self.command.startswith('G1'):\r
+ self.parent.machineCom.sendCommand("G90")\r
e.Skip()\r
\r
class printWindow(wx.Frame):\r
def __init__(self):\r
super(printWindow, self).__init__(None, -1, title='Printing')\r
self.machineCom = None\r
- self.machineConnected = False\r
- self.thread = None\r
self.gcode = None\r
self.gcodeList = None\r
self.sendList = []\r
- self.printIdx = None\r
self.temp = None\r
self.bedTemp = None\r
self.bufferLineCount = 4\r
\r
sb = wx.StaticBox(self.panel, label="Statistics")\r
boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL)\r
- self.statsText = wx.StaticText(self.panel, -1, "Filament: ####.##m #.##g\nPrint time: #####:##")\r
+ self.statsText = wx.StaticText(self.panel, -1, "Filament: ####.##m #.##g\nPrint time: #####:##\nMachine state: Detecting baudrate")\r
boxsizer.Add(self.statsText, flag=wx.LEFT, border=5)\r
\r
self.sizer.Add(boxsizer, pos=(0,0), span=(5,1), flag=wx.EXPAND)\r
self.Centre()\r
\r
self.UpdateButtonStates()\r
- self.UpdateProgress()\r
+ #self.UpdateProgress()\r
\r
def OnCameraTimer(self, e):\r
- if self.printIdx != None:\r
+ if self.machineCom != None and not self.machineCom.isPrinting():\r
return\r
self.cam.takeNewImage()\r
self.camPage.Refresh()\r
dc.DrawBitmap(self.cam.getLastImage(), 0, 0)\r
\r
def UpdateButtonStates(self):\r
- self.connectButton.Enable(not self.machineConnected)\r
+ self.connectButton.Enable(self.machineCom == None or self.machineCom.isClosedOrError())\r
#self.loadButton.Enable(self.printIdx == None)\r
- self.printButton.Enable(self.machineConnected and self.gcodeList != None and self.printIdx == None)\r
- self.pauseButton.Enable(self.printIdx != None)\r
- self.cancelButton.Enable(self.printIdx != None)\r
- self.temperatureSelect.Enable(self.machineConnected)\r
- self.bedTemperatureSelect.Enable(self.machineConnected)\r
- self.directControlPanel.Enable(self.machineConnected)\r
+ self.printButton.Enable(self.machineCom != None and self.machineCom.isOperational() and not self.machineCom.isPrinting())\r
+ self.pauseButton.Enable(self.machineCom != None and self.machineCom.isPrinting())\r
+ self.cancelButton.Enable(self.machineCom != None and self.machineCom.isPrinting())\r
+ self.temperatureSelect.Enable(self.machineCom != None and self.machineCom.isOperational())\r
+ self.bedTemperatureSelect.Enable(self.machineCom != None and self.machineCom.isOperational())\r
+ self.directControlPanel.Enable(self.machineCom != None and self.machineCom.isOperational())\r
\r
def UpdateProgress(self):\r
status = ""\r
if cost != False:\r
status += "Filament cost: %s\n" % (cost)\r
status += "Print time: %02d:%02d\n" % (int(self.gcode.totalMoveTimeMinute / 60), int(self.gcode.totalMoveTimeMinute % 60))\r
- if self.printIdx == None:\r
+ if self.machineCom == None or not self.machineCom.isPrinting():\r
self.progress.SetValue(0)\r
if self.gcodeList != None:\r
status += 'Line: -/%d\n' % (len(self.gcodeList))\r
else:\r
- status += 'Line: %d/%d\n' % (self.printIdx, len(self.gcodeList))\r
+ status += 'Line: %d/%d\n' % (self.machineCom.getPrintPos(), len(self.gcodeList))\r
status += 'Height: %f\n' % (self.currentZ)\r
- self.progress.SetValue(self.printIdx)\r
- if self.temp != None:\r
- status += 'Temp: %d\n' % (self.temp)\r
- if self.bedTemp != None and self.bedTemp > 0:\r
- status += 'Bed Temp: %d\n' % (self.bedTemp)\r
- self.bedTemperatureLabel.Show(True)\r
- self.bedTemperatureSelect.Show(True)\r
- self.temperaturePanel.Layout()\r
+ self.progress.SetValue(self.machineCom.getPrintPos())\r
+ if self.machineCom != None:\r
+ if self.machineCom.getTemp() > 0:\r
+ status += 'Temp: %d\n' % (self.machineCom.getTemp())\r
+ if self.machineCom.getBedTemp() > 0:\r
+ status += 'Bed Temp: %d\n' % (self.machineCom.getBedTemp())\r
+ self.bedTemperatureLabel.Show(True)\r
+ self.bedTemperatureSelect.Show(True)\r
+ self.temperaturePanel.Layout()\r
+ status += 'Machine state: %s\n' % (self.machineCom.getStateString())\r
+ \r
self.statsText.SetLabel(status.strip())\r
- #self.Layout()\r
\r
def OnConnect(self, e):\r
if self.machineCom != None:\r
self.machineCom.close()\r
- self.thread.join()\r
- self.machineCom = machineCom.MachineCom()\r
- self.thread = threading.Thread(target=self.PrinterMonitor)\r
- self.thread.start()\r
+ self.machineCom = machineCom.MachineCom(callbackObject=self)\r
self.UpdateButtonStates()\r
\r
def OnLoad(self, e):\r
pass\r
\r
def OnPrint(self, e):\r
- if not self.machineConnected:\r
+ if self.machineCom == None or not self.machineCom.isOperational():\r
return\r
if self.gcodeList == None:\r
return\r
- if self.printIdx != None:\r
+ if self.machineCom.isPrinting():\r
return\r
self.currentZ = -1\r
if self.cam != None:\r
self.cam.startTimelaps(self.filename[: self.filename.rfind('.')] + ".mpg")\r
- self.printIdx = 1\r
- self.sendLine(0)\r
- self.sendCnt = self.bufferLineCount\r
+ self.machineCom.printGCode(self.gcodeList)\r
self.UpdateButtonStates()\r
\r
def OnCancel(self, e):\r
if self.cam != None:\r
self.cam.endTimelaps()\r
- self.printIdx = None\r
- self.pause = False\r
self.pauseButton.SetLabel('Pause')\r
- self.sendCommand("M84")\r
+ self.machineCom.cancelPrint()\r
+ self.machineCom.sendCommand("M84")\r
self.UpdateButtonStates()\r
\r
def OnPause(self, e):\r
- if self.pause:\r
- self.pause = False\r
- self.sendLine(self.printIdx)\r
- self.printIdx += 1\r
+ if self.machineCom.isPaused():\r
+ self.machineCom.setPause(False)\r
self.pauseButton.SetLabel('Pause')\r
else:\r
- self.pause = True\r
+ self.machineCom.setPause(True)\r
self.pauseButton.SetLabel('Resume')\r
\r
def OnClose(self, e):\r
printWindowHandle = None\r
if self.machineCom != None:\r
self.machineCom.close()\r
- self.thread.join()\r
self.Destroy()\r
\r
def OnTempChange(self, e):\r
- self.sendCommand("M104 S%d" % (self.temperatureSelect.GetValue()))\r
+ self.machineCom.sendCommand("M104 S%d" % (self.temperatureSelect.GetValue()))\r
\r
def OnBedTempChange(self, e):\r
- self.sendCommand("M140 S%d" % (self.bedTemperatureSelect.GetValue()))\r
+ self.machineCom.sendCommand("M140 S%d" % (self.bedTemperatureSelect.GetValue()))\r
\r
def OnSpeedChange(self, e):\r
self.feedrateRatioOuterWall = self.outerWallSpeedSelect.GetValue() / 100.0\r
if line == '':\r
return\r
self.termLog.AppendText('>%s\n' % (line))\r
- self.sendCommand(line)\r
+ self.machineCom.sendCommand(line)\r
self.termHistory.append(line)\r
self.termHistoryIdx = len(self.termHistory)\r
self.termInput.SetValue('')\r
e.Skip()\r
\r
def LoadGCodeFile(self, filename):\r
- if self.printIdx != None:\r
+ if self.machineCom != None and self.machineCom.isPrinting():\r
return\r
#Send an initial M110 to reset the line counter to zero.\r
lineType = 'CUSTOM'\r
wx.CallAfter(self.UpdateButtonStates)\r
wx.CallAfter(self.UpdateProgress)\r
\r
- def sendCommand(self, cmd):\r
- if self.machineConnected:\r
- if self.printIdx == None or self.pause:\r
- self.machineCom.sendCommand(cmd)\r
- else:\r
- self.sendList.append(cmd)\r
-\r
def sendLine(self, lineNr):\r
if lineNr >= len(self.gcodeList):\r
return False\r
self.machineCom.sendCommand("N%d%s*%d" % (lineNr, line, checksum))\r
return True\r
\r
- def PrinterMonitor(self):\r
- while True:\r
- line = self.machineCom.readline()\r
- if line == None:\r
- self.machineConnected = False\r
- wx.CallAfter(self.UpdateButtonStates)\r
- return\r
- if line == '': #When we have a communication "timeout" and we're not sending gcode, then read the temperature.\r
- if self.printIdx == None or self.pause:\r
- self.machineCom.sendCommand("M105")\r
- else:\r
- wx.CallAfter(self.AddTermLog, '!!Comm timeout, forcing next line!!\n')\r
- line = 'ok'\r
- if self.machineConnected:\r
- while self.sendCnt > 0 and not self.pause:\r
- self.sendLine(self.printIdx)\r
- self.printIdx += 1\r
- self.sendCnt -= 1\r
- if line.startswith("start"):\r
- self.machineConnected = True\r
- wx.CallAfter(self.UpdateButtonStates)\r
- elif 'T:' in line:\r
- self.temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))\r
- if 'B:' in line:\r
- self.bedTemp = float(re.search("[0-9\.]*", line.split('B:')[1]).group(0))\r
- self.temperatureGraph.addPoint(self.temp, self.temperatureSelect.GetValue(), self.bedTemp, self.bedTemperatureSelect.GetValue())\r
- wx.CallAfter(self.UpdateProgress)\r
- elif line.strip() != 'ok':\r
- wx.CallAfter(self.AddTermLog, line)\r
- if self.printIdx != None:\r
- if line.startswith("ok"):\r
- if len(self.sendList) > 0:\r
- self.machineCom.sendCommand(self.sendList.pop(0))\r
- elif self.pause:\r
- self.sendCnt += 1\r
- else:\r
- if self.sendLine(self.printIdx):\r
- self.printIdx += 1\r
- else:\r
- if self.cam != None:\r
- self.cam.endTimelaps()\r
- self.printIdx = None\r
- wx.CallAfter(self.UpdateButtonStates)\r
- wx.CallAfter(self.UpdateProgress)\r
- elif "resend" in line.lower() or "rs" in line:\r
- try:\r
- lineNr=int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])\r
- except:\r
- if "rs" in line:\r
- lineNr=int(line.split()[1])\r
- self.printIdx = lineNr\r
- #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.\r
+ def mcLog(self, message):\r
+ #print message\r
+ pass\r
+ \r
+ def mcTempUpdate(self, temp, bedTemp):\r
+ self.temperatureGraph.addPoint(temp, self.temperatureSelect.GetValue(), bedTemp, self.bedTemperatureSelect.GetValue())\r
+ \r
+ def mcStateChange(self, state):\r
+ wx.CallAfter(self.UpdateButtonStates)\r
+ wx.CallAfter(self.UpdateProgress)\r
+ \r
+ def mcMessage(self, message):\r
+ wx.CallAfter(self.AddTermLog, message)\r
+ \r
+ def mcProgress(self, lineNr):\r
+ wx.CallAfter(self.UpdateProgress)\r
\r
class temperatureGraph(wx.Panel):\r
def __init__(self, parent):\r
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
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:
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())))
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))
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]