chiark / gitweb /
Keep updating the temperature even in paused state
[cura.git] / Cura / util / machineCom.py
1 """
2 MachineCom handles communication with GCode based printers trough (USB) serial ports.
3 For actual printing of objects this module is used from Cura.serialCommunication and ran in a separate process.
4 """
5 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
6
7 import os
8 import glob
9 import sys
10 import time
11 import math
12 import re
13 import traceback
14 import threading
15 import platform
16 import Queue as queue
17
18 import serial
19
20 from Cura.avr_isp import stk500v2
21 from Cura.avr_isp import ispBase
22
23 from Cura.util import profile
24 from Cura.util import version
25
26 try:
27         import _winreg
28 except:
29         pass
30
31 def serialList(forAutoDetect=False):
32         """
33                 Retrieve a list of serial ports found in the system.
34         :param forAutoDetect: if true then only the USB serial ports are listed. Else all ports are listed.
35         :return: A list of strings where each string is a serial port.
36         """
37         baselist=[]
38         if platform.system() == "Windows":
39                 try:
40                         key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
41                         i=0
42                         while True:
43                                 values = _winreg.EnumValue(key, i)
44                                 if not forAutoDetect or 'USBSER' in values[0]:
45                                         baselist+=[values[1]]
46                                 i+=1
47                 except:
48                         pass
49         if forAutoDetect:
50                 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.usb*")
51                 baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
52                 prev = profile.getMachineSetting('serial_port_auto')
53                 if prev in baselist:
54                         baselist.remove(prev)
55                         baselist.insert(0, prev)
56         else:
57                 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob('/dev/serial/by-id/*')
58         if version.isDevVersion() and not forAutoDetect:
59                 baselist.append('VIRTUAL')
60         return baselist
61
62 def baudrateList():
63         """
64         :return: a list of integers containing all possible baudrates at which we can communicate.
65                         Used for auto-baudrate detection as well as manual baudrate selection.
66         """
67         ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
68         if profile.getMachineSetting('serial_baud_auto') != '':
69                 prev = int(profile.getMachineSetting('serial_baud_auto'))
70                 if prev in ret:
71                         ret.remove(prev)
72                         ret.insert(0, prev)
73         return ret
74
75 class VirtualPrinter():
76         """
77         A virtual printer class used for debugging. Acts as a serial.Serial class, but without connecting to any port.
78         Only available when running the development version of Cura.
79         """
80         def __init__(self):
81                 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
82                 self.temp = 0.0
83                 self.targetTemp = 0.0
84                 self.lastTempAt = time.time()
85                 self.bedTemp = 1.0
86                 self.bedTargetTemp = 1.0
87         
88         def write(self, data):
89                 if self.readList is None:
90                         return
91                 #print "Send: %s" % (data.rstrip())
92                 if 'M104' in data or 'M109' in data:
93                         try:
94                                 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
95                         except:
96                                 pass
97                 if 'M140' in data or 'M190' in data:
98                         try:
99                                 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
100                         except:
101                                 pass
102                 if 'M105' in data:
103                         self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
104                 elif len(data.strip()) > 0:
105                         self.readList.append("ok\n")
106
107         def readline(self):
108                 if self.readList is None:
109                         return ''
110                 n = 0
111                 timeDiff = self.lastTempAt - time.time()
112                 self.lastTempAt = time.time()
113                 if abs(self.temp - self.targetTemp) > 1:
114                         self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
115                 if abs(self.bedTemp - self.bedTargetTemp) > 1:
116                         self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
117                 while len(self.readList) < 1:
118                         time.sleep(0.1)
119                         n += 1
120                         if n == 20:
121                                 return ''
122                         if self.readList is None:
123                                 return ''
124                 time.sleep(0.001)
125                 #print "Recv: %s" % (self.readList[0].rstrip())
126                 return self.readList.pop(0)
127         
128         def close(self):
129                 self.readList = None
130
131 class MachineComPrintCallback(object):
132         """
133         Base class for callbacks from the MachineCom class.
134         This class has all empty implementations and is attached to the MachineCom if no other callback object is attached.
135         """
136         def mcLog(self, message):
137                 pass
138         
139         def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
140                 pass
141         
142         def mcStateChange(self, state):
143                 pass
144         
145         def mcMessage(self, message):
146                 pass
147         
148         def mcProgress(self, lineNr):
149                 pass
150         
151         def mcZChange(self, newZ):
152                 pass
153
154 class MachineCom(object):
155         """
156         Class for (USB) serial communication with 3D printers.
157         This class keeps track of if the connection is still live, can auto-detect serial ports and baudrates.
158         """
159         STATE_NONE = 0
160         STATE_OPEN_SERIAL = 1
161         STATE_DETECT_SERIAL = 2
162         STATE_DETECT_BAUDRATE = 3
163         STATE_CONNECTING = 4
164         STATE_OPERATIONAL = 5
165         STATE_PRINTING = 6
166         STATE_PAUSED = 7
167         STATE_CLOSED = 8
168         STATE_ERROR = 9
169         STATE_CLOSED_WITH_ERROR = 10
170         
171         def __init__(self, port = None, baudrate = None, callbackObject = None):
172                 if port is None:
173                         port = profile.getMachineSetting('serial_port')
174                 if baudrate is None:
175                         if profile.getMachineSetting('serial_baud') == 'AUTO':
176                                 baudrate = 0
177                         else:
178                                 baudrate = int(profile.getMachineSetting('serial_baud'))
179                 if callbackObject is None:
180                         callbackObject = MachineComPrintCallback()
181
182                 self._port = port
183                 self._baudrate = baudrate
184                 self._callback = callbackObject
185                 self._state = self.STATE_NONE
186                 self._serial = None
187                 self._serialDetectList = []
188                 self._baudrateDetectList = baudrateList()
189                 self._baudrateDetectRetry = 0
190                 self._extruderCount = int(profile.getMachineSetting('extruder_amount'))
191                 self._temperatureRequestExtruder = 0
192                 self._temp = [0] * self._extruderCount
193                 self._targetTemp = [0] * self._extruderCount
194                 self._bedTemp = 0
195                 self._bedTargetTemp = 0
196                 self._gcodeList = None
197                 self._gcodePos = 0
198                 self._commandQueue = queue.Queue()
199                 self._logQueue = queue.Queue(256)
200                 self._feedRateModifier = {}
201                 self._currentZ = -1
202                 self._heatupWaitStartTime = 0
203                 self._heatupWaitTimeLost = 0.0
204                 self._printStartTime100 = None
205                 self._currentCommands = []
206
207                 self._thread_lock = threading.Lock()
208                 self.thread = threading.Thread(target=self._monitor)
209                 self.thread.daemon = True
210                 self.thread.start()
211         
212         def _changeState(self, newState):
213                 if self._state == newState:
214                         return
215                 oldState = self.getStateString()
216                 self._state = newState
217                 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
218                 self._callback.mcStateChange(newState)
219         
220         def getState(self):
221                 return self._state
222         
223         def getStateString(self):
224                 if self._state == self.STATE_NONE:
225                         return "Offline"
226                 if self._state == self.STATE_OPEN_SERIAL:
227                         return "Opening serial port"
228                 if self._state == self.STATE_DETECT_SERIAL:
229                         return "Detecting serial port"
230                 if self._state == self.STATE_DETECT_BAUDRATE:
231                         return "Detecting baudrate"
232                 if self._state == self.STATE_CONNECTING:
233                         return "Connecting"
234                 if self._state == self.STATE_OPERATIONAL:
235                         return "Operational"
236                 if self._state == self.STATE_PRINTING:
237                         return "Printing"
238                 if self._state == self.STATE_PAUSED:
239                         return "Paused"
240                 if self._state == self.STATE_CLOSED:
241                         return "Closed"
242                 if self._state == self.STATE_ERROR:
243                         return "Error: %s" % (self.getShortErrorString())
244                 if self._state == self.STATE_CLOSED_WITH_ERROR:
245                         return "Error: %s" % (self.getShortErrorString())
246                 return "?%d?" % (self._state)
247         
248         def getShortErrorString(self):
249                 if len(self._errorValue) < 35:
250                         return self._errorValue
251                 return self._errorValue[:35] + "..."
252
253         def getErrorString(self):
254                 return self._errorValue
255
256         def isClosed(self):
257                 return self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
258
259         def isClosedOrError(self):
260                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
261
262         def isError(self):
263                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
264         
265         def isOperational(self):
266                 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
267         
268         def isPrinting(self):
269                 return self._state == self.STATE_PRINTING
270         
271         def isPaused(self):
272                 return self._state == self.STATE_PAUSED
273
274         def getPrintPos(self):
275                 return self._gcodePos
276         
277         def getPrintTime(self):
278                 return time.time() - self._printStartTime
279
280         def getPrintTimeRemainingEstimate(self):
281                 if self._printStartTime100 is None or self.getPrintPos() < 200:
282                         return None
283                 printTime = (time.time() - self._printStartTime100) / 60
284                 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
285                 printTimeLeft = printTimeTotal - printTime
286                 return printTimeLeft
287         
288         def getTemp(self):
289                 return self._temp
290         
291         def getBedTemp(self):
292                 return self._bedTemp
293         
294         def getLog(self):
295                 ret = []
296                 while not self._logQueue.empty():
297                         ret.append(self._logQueue.get())
298                 for line in ret:
299                         self._logQueue.put(line, False)
300                 return ret
301
302         def receivedOK(self):
303                 if len(self._currentCommands) > 0:
304                         # Marlin will answer 'ok' immediatly to G[0-3] commands
305                         for i in xrange(0, len(self._currentCommands)):
306                                 if "G0 " in self._currentCommands[i] or \
307                                    "G1 " in self._currentCommands[i] or \
308                                    "G2 " in self._currentCommands[i] or \
309                                    "G3 " in self._currentCommands[i]:
310                                         self._currentCommands.pop(i)
311                                         return
312                         self._currentCommands.pop(0)
313
314         def _monitor(self):
315                 #Open the serial port.
316                 if self._port == 'AUTO':
317                         self._changeState(self.STATE_DETECT_SERIAL)
318                         programmer = stk500v2.Stk500v2()
319                         for p in serialList(True):
320                                 try:
321                                         self._log("Connecting to: %s (programmer)" % (p))
322                                         programmer.connect(p)
323                                         self._serial = programmer.leaveISP()
324                                         profile.putMachineSetting('serial_port_auto', p)
325                                         break
326                                 except ispBase.IspError as (e):
327                                         self._log("Error while connecting to %s: %s" % (p, str(e)))
328                                         pass
329                                 except:
330                                         self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
331                                 programmer.close()
332                         if self._serial is None:
333                                 self._log("Serial port list: %s" % (str(serialList(True))))
334                                 self._serialDetectList = serialList(True)
335                 elif self._port == 'VIRTUAL':
336                         self._changeState(self.STATE_OPEN_SERIAL)
337                         self._serial = VirtualPrinter()
338                 else:
339                         self._changeState(self.STATE_OPEN_SERIAL)
340                         try:
341                                 if self._baudrate == 0:
342                                         self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
343                                         self._serial = serial.Serial(str(self._port), 115200, timeout=3, writeTimeout=10000)
344                                 else:
345                                         self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
346                                         self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5, writeTimeout=10000)
347                         except:
348                                 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
349                 if self._serial is None:
350                         baudrate = self._baudrate
351                         if baudrate == 0:
352                                 baudrate = self._baudrateDetectList.pop(0)
353                         if len(self._serialDetectList) < 1:
354                                 self._log("Found no ports to try for auto detection")
355                                 self._errorValue = 'Failed to autodetect serial port.'
356                                 self._changeState(self.STATE_ERROR)
357                                 return
358                         port = self._serialDetectList.pop(0)
359                         self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
360                         try:
361                                 self._serial = serial.Serial(port, baudrate, timeout=3, writeTimeout=10000)
362                         except:
363                                 pass
364                 else:
365                         self._log("Connected to: %s, starting monitor" % (self._serial))
366                         if self._baudrate == 0:
367                                 self._changeState(self.STATE_DETECT_BAUDRATE)
368                         else:
369                                 self._changeState(self.STATE_CONNECTING)
370
371                 #Start monitoring the serial port.
372                 if self._state == self.STATE_CONNECTING:
373                         timeout = time.time() + 15
374                 else:
375                         timeout = time.time() + 5
376                 tempRequestTimeout = timeout
377                 while True:
378                         line = self._readline()
379                         if line is None:
380                                 break
381
382                         #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
383                         # Only goto error on known fatal errors.
384                         if line.startswith('Error:'):
385                                 #Oh YEAH, consistency.
386                                 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
387                                 #       But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
388                                 #       So we can have an extra newline in the most common case. Awesome work people.
389                                 if re.match('Error:[0-9]\n', line):
390                                         line = line.rstrip() + self._readline()
391                                 #Skip the communication errors, as those get corrected.
392                                 if 'Extruder switched off' in line or 'Temperature heated bed switched off' in line or 'Something is wrong, please turn off the printer.' in line:
393                                         if not self.isError():
394                                                 self._errorValue = line[6:]
395                                                 self._changeState(self.STATE_ERROR)
396                         if ' T:' in line or line.startswith('T:'):
397                                 tempRequestTimeout = time.time() + 5
398                                 try:
399                                         self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
400                                 except:
401                                         pass
402                                 if 'B:' in line:
403                                         try:
404                                                 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
405                                         except:
406                                                 pass
407                                 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
408                                 #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.
409                                 if not 'ok' in line and self._heatupWaitStartTime != 0:
410                                         t = time.time()
411                                         self._heatupWaitTimeLost = t - self._heatupWaitStartTime
412                                         self._heatupWaitStartTime = t
413                         elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
414                                  not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
415                                  not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
416                                  line != 'echo:Unknown command:""\n' and self.isOperational():
417                                 self._callback.mcMessage(line)
418
419                         if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
420                                 if line == '' or time.time() > timeout:
421                                         if len(self._baudrateDetectList) < 1:
422                                                 self.close()
423                                                 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
424                                                 self._changeState(self.STATE_ERROR)
425                                         elif self._baudrateDetectRetry > 0:
426                                                 self._baudrateDetectRetry -= 1
427                                                 self._serial.write('\n')
428                                                 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
429                                                 self._sendCommand("M105")
430                                                 self._testingBaudrate = True
431                                         else:
432                                                 if self._state == self.STATE_DETECT_SERIAL:
433                                                         if len(self._serialDetectList) == 0:
434                                                                 if len(self._baudrateDetectList) == 0:
435                                                                         self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
436                                                                         self._errorValue = 'Failed to autodetect serial port.'
437                                                                         self._changeState(self.STATE_ERROR)
438                                                                         return
439                                                                 else:
440                                                                         self._serialDetectList = serialList(True)
441                                                                         baudrate = self._baudrateDetectList.pop(0)
442                                                         self._serial.close()
443                                                         self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
444                                                 else:
445                                                         baudrate = self._baudrateDetectList.pop(0)
446                                                 try:
447                                                         self._setBaudrate(baudrate)
448                                                         self._serial.timeout = 0.5
449                                                         self._log("Trying baudrate: %d" % (baudrate))
450                                                         self._baudrateDetectRetry = 5
451                                                         self._baudrateDetectTestOk = 0
452                                                         timeout = time.time() + 5
453                                                         self._serial.write('\n')
454                                                         self._sendCommand("M105")
455                                                         self._testingBaudrate = True
456                                                 except:
457                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
458                                 elif 'T:' in line:
459                                         self._baudrateDetectTestOk += 1
460                                         if self._baudrateDetectTestOk < 10:
461                                                 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
462                                                 self._sendCommand("M105")
463                                         else:
464                                                 self._sendCommand("M999")
465                                                 self._serial.timeout = 2
466                                                 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
467                                                 self._changeState(self.STATE_OPERATIONAL)
468                                 else:
469                                         self._testingBaudrate = False
470                         elif self._state == self.STATE_CONNECTING:
471                                 if line == '' or 'wait' in line or 'start' in line:        # 'wait' needed for Repetier (kind of watchdog)
472                                         self._sendCommand("M105")
473                                 elif 'ok' in line:
474                                         self._changeState(self.STATE_OPERATIONAL)
475                                 if time.time() > timeout:
476                                         self.close()
477                         elif self._state == self.STATE_OPERATIONAL:
478                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
479                                 if line == '':
480                                         if self._extruderCount > 0:
481                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
482                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
483                                         else:
484                                                 self.sendCommand("M105")
485                                         tempRequestTimeout = time.time() + 5
486                                 elif 'ok' in line:
487                                         self.receivedOK()
488                                 elif 'start' in line:
489                                         self._currentCommands = []
490                         elif self._state == self.STATE_PRINTING:
491                                 #Even when printing request the temperature every 5 seconds.
492                                 if time.time() > tempRequestTimeout:
493                                         if self._extruderCount > 0:
494                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
495                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
496                                         else:
497                                                 self.sendCommand("M105")
498                                         tempRequestTimeout = time.time() + 5
499                                 if line == '' and time.time() > timeout:
500                                         self._log("Communication timeout during printing, forcing a line")
501                                         line = 'ok'
502                                 if 'ok' in line:
503                                         self.receivedOK()
504                                         timeout = time.time() + 5
505                                         if not self._commandQueue.empty():
506                                                 self._sendCommand(self._commandQueue.get())
507                                         else:
508                                                 self._sendNext()
509                                         if "G28" in self._currentCommands[0] or "G29" in self._currentCommands[0] or \
510                                            "M109" in self._currentCommands[0] or "M190" in self._currentCommands[0]:
511                                                 # Long command detected. Timeout is now set to 60s to avoid forcing 'ok'
512                                                 # every 5 seconds while it's not needed
513                                                 timeout = time.time() + 60
514
515                                 elif 'start' in line:
516                                         self._currentCommands = []
517                                 elif "resend" in line.lower() or "rs" in line:
518                                         newPos = self._gcodePos
519                                         try:
520                                                 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
521                                         except:
522                                                 if "rs" in line:
523                                                         newPos = int(line.split()[1])
524                                         # If we need to resend more than 10 lines, we can assume that the machine
525                                         # was shut down and turned back on or something else that's weird just happened.
526                                         # In that case, it can be dangerous to restart the print, so we'd better kill it
527                                         if newPos == 1 or self._gcodePos > newPos + 100:
528                                                 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
529                                                 self.cancelPrint()
530                                         else:
531                                                 self._gcodePos = newPos
532                         elif self._state == self.STATE_PAUSED:
533                                 #Even when printing request the temperature every 5 seconds.
534                                 if time.time() > tempRequestTimeout:
535                                         if self._extruderCount > 0:
536                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
537                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
538                                         else:
539                                                 self.sendCommand("M105")
540                                         tempRequestTimeout = time.time() + 5
541                                 if line == '' and time.time() > timeout:
542                                         line = 'ok'
543                                 if 'ok' in line:
544                                         self.receivedOK()
545                                         timeout = time.time() + 5
546                                         if not self._commandQueue.empty():
547                                                 self._sendCommand(self._commandQueue.get())
548                                 elif 'start' in line:
549                                         self._currentCommands = []
550
551                 self._log("Connection closed, closing down monitor")
552
553         def _setBaudrate(self, baudrate):
554                 try:
555                         self._serial.baudrate = baudrate
556                 except:
557                         print getExceptionString()
558
559         def _log(self, message):
560                 #sys.stderr.write(message + "\n");
561                 self._callback.mcLog(message)
562                 try:
563                         self._logQueue.put(message, False)
564                 except:
565                         #If the log queue is full, remove the first message and append the new message again
566                         self._logQueue.get()
567                         try:
568                                 self._logQueue.put(message, False)
569                         except:
570                                 pass
571
572         def _readline(self):
573                 if self._serial is None:
574                         return None
575                 try:
576                         ret = self._serial.readline()
577                 except:
578                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
579                         self._errorValue = getExceptionString()
580                         self.close(True)
581                         return None
582                 if ret == '':
583                         #self._log("Recv: TIMEOUT")
584                         return ''
585                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
586                 return ret
587         
588         def close(self, isError = False):
589                 if self._serial != None:
590                         self._serial.close()
591                         if isError:
592                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
593                         else:
594                                 self._changeState(self.STATE_CLOSED)
595                 self._serial = None
596         
597         def __del__(self):
598                 self.close()
599         
600         def _sendCommand(self, cmd):
601                 self._thread_lock.acquire(True)
602                 if self._serial is None:
603                         self._thread_lock.release()
604                         return
605                 if 'M109' in cmd or 'M190' in cmd:
606                         self._heatupWaitStartTime = time.time()
607                 if 'M104' in cmd or 'M109' in cmd:
608                         try:
609                                 t = 0
610                                 if 'T' in cmd:
611                                         t = int(re.search('T([0-9]+)', cmd).group(1))
612                                 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
613                         except:
614                                 pass
615                 if 'M140' in cmd or 'M190' in cmd:
616                         try:
617                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
618                         except:
619                                 pass
620                 self._log('Send: %s' % (cmd))
621                 if self.isOperational():
622                         self._currentCommands.append(cmd)
623                 try:
624                         self._serial.write(cmd + '\n')
625                 except serial.SerialTimeoutException:
626                         self._log("Serial timeout while writing to serial port, trying again.")
627                         try:
628                                 time.sleep(0.5)
629                                 self._serial.write(cmd + '\n')
630                         except:
631                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
632                                 self._errorValue = getExceptionString()
633                                 self.close(True)
634                 except:
635                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
636                         self._errorValue = getExceptionString()
637                         self.close(True)
638                 self._thread_lock.release()
639
640         def _sendNext(self):
641                 if self._gcodePos >= len(self._gcodeList):
642                         self._changeState(self.STATE_OPERATIONAL)
643                         return
644                 if self._gcodePos == 100:
645                         self._printStartTime100 = time.time()
646                 line = self._gcodeList[self._gcodePos]
647                 if type(line) is tuple:
648                         self._printSection = line[1]
649                         line = line[0]
650                 try:
651                         if line == 'M0' or line == 'M1':
652                                 self.setPause(True)
653                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
654                         if self._printSection in self._feedRateModifier:
655                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
656                         if ('G0' in line or 'G1' in line) and 'Z' in line:
657                                 z = float(re.search('Z(-?[0-9\.]*)', line).group(1))
658                                 if self._currentZ != z:
659                                         self._currentZ = z
660                                         self._callback.mcZChange(z)
661                 except:
662                         self._log("Unexpected error: %s" % (getExceptionString()))
663                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
664                 pos = self._gcodePos
665                 self._gcodePos += 1
666                 self._sendCommand("N%d%s*%d" % (pos, line, checksum))
667                 self._callback.mcProgress(self._gcodePos)
668
669         def sendCommand(self, cmd):
670                 cmd = cmd.encode('ascii', 'replace')
671                 if self.isPrinting() or self.isPaused():
672                         self._commandQueue.put(cmd)
673                         if len(self._currentCommands) == 0:
674                                 self._sendCommand(self._commandQueue.get())
675                 elif self.isOperational():
676                         self._sendCommand(cmd)
677
678         def printGCode(self, gcodeList):
679                 if not self.isOperational() or self.isPrinting() or self.isPaused():
680                         return
681                 self._gcodeList = gcodeList
682                 self._gcodePos = 0
683                 self._printStartTime100 = None
684                 self._printSection = 'CUSTOM'
685                 self._changeState(self.STATE_PRINTING)
686                 self._printStartTime = time.time()
687                 for i in xrange(0, 4):
688                         self._sendNext()
689         
690         def cancelPrint(self):
691                 if self.isOperational():
692                         self._changeState(self.STATE_OPERATIONAL)
693         
694         def setPause(self, pause):
695                 if not pause and self.isPaused():
696                         self._changeState(self.STATE_PRINTING)
697                         for i in xrange(0, 4):
698                                 if not self._commandQueue.empty():
699                                         self._sendCommand(self._commandQueue.get())
700                                 else:
701                                         self._sendNext()
702                 if pause and self.isPrinting():
703                         self._changeState(self.STATE_PAUSED)
704         
705         def setFeedrateModifier(self, type, value):
706                 self._feedRateModifier[type] = value
707
708 def getExceptionString():
709         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
710         return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])