chiark / gitweb /
plugins: Support user configuration of default values
[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 from Cura.util import serialWrapper as 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)
344                                         # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
345                                         self._serial.writeTimeout=10000
346                                 else:
347                                         self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
348                                         self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5)
349                                         # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
350                                         self._serial.writeTimeout=10000
351                         except:
352                                 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
353                 if self._serial is None:
354                         baudrate = self._baudrate
355                         if baudrate == 0:
356                                 baudrate = self._baudrateDetectList.pop(0)
357                         if len(self._serialDetectList) < 1:
358                                 self._log("Found no ports to try for auto detection")
359                                 self._errorValue = 'Failed to autodetect serial port.'
360                                 self._changeState(self.STATE_ERROR)
361                                 return
362                         port = self._serialDetectList.pop(0)
363                         self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
364                         try:
365                                 self._serial = serial.Serial(port, baudrate, timeout=3)
366                                 # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
367                                 self._serial.writeTimeout=10000
368                         except:
369                                 pass
370                 else:
371                         self._log("Connected to: %s, starting monitor" % (self._serial))
372                         if self._baudrate == 0:
373                                 self._changeState(self.STATE_DETECT_BAUDRATE)
374                         else:
375                                 self._changeState(self.STATE_CONNECTING)
376
377                 #Start monitoring the serial port.
378                 if self._state == self.STATE_CONNECTING:
379                         timeout = time.time() + 15
380                 else:
381                         timeout = time.time() + 5
382                 tempRequestTimeout = timeout
383                 while True:
384                         line = self._readline()
385                         if line is None:
386                                 break
387
388                         #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
389                         # Only goto error on known fatal errors.
390                         if line.startswith('Error:'):
391                                 #Oh YEAH, consistency.
392                                 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
393                                 #       But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
394                                 #       So we can have an extra newline in the most common case. Awesome work people.
395                                 if re.match('Error:[0-9]\n', line):
396                                         line = line.rstrip() + self._readline()
397                                 #Skip the communication errors, as those get corrected.
398                                 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:
399                                         if not self.isError():
400                                                 self._errorValue = line[6:]
401                                                 self._changeState(self.STATE_ERROR)
402                         if ' T:' in line or line.startswith('T:'):
403                                 tempRequestTimeout = time.time() + 5
404                                 try:
405                                         self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
406                                 except:
407                                         pass
408                                 if 'B:' in line:
409                                         try:
410                                                 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
411                                         except:
412                                                 pass
413                                 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
414                                 #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.
415                                 if not 'ok' in line and self._heatupWaitStartTime != 0:
416                                         t = time.time()
417                                         self._heatupWaitTimeLost = t - self._heatupWaitStartTime
418                                         self._heatupWaitStartTime = t
419                         elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
420                                  not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
421                                  not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
422                                  line != 'echo:Unknown command:""\n' and self.isOperational():
423                                 self._callback.mcMessage(line)
424
425                         if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
426                                 if line == '' or time.time() > timeout:
427                                         if len(self._baudrateDetectList) < 1:
428                                                 self.close()
429                                                 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
430                                                 self._changeState(self.STATE_ERROR)
431                                         elif self._baudrateDetectRetry > 0:
432                                                 self._baudrateDetectRetry -= 1
433                                                 self._serial.write('\n')
434                                                 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
435                                                 self._sendCommand("M105")
436                                                 self._testingBaudrate = True
437                                         else:
438                                                 if self._state == self.STATE_DETECT_SERIAL:
439                                                         if len(self._serialDetectList) == 0:
440                                                                 if len(self._baudrateDetectList) == 0:
441                                                                         self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
442                                                                         self._errorValue = 'Failed to autodetect serial port.'
443                                                                         self._changeState(self.STATE_ERROR)
444                                                                         return
445                                                                 else:
446                                                                         self._serialDetectList = serialList(True)
447                                                                         baudrate = self._baudrateDetectList.pop(0)
448                                                         self._serial.close()
449                                                         self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5)
450                                                         # Need to set writeTimeout separately in order to be compatible with pyserial 3.0
451                                                         self._serial.writeTimeout=10000
452                                                 else:
453                                                         baudrate = self._baudrateDetectList.pop(0)
454                                                 try:
455                                                         self._setBaudrate(baudrate)
456                                                         self._serial.timeout = 0.5
457                                                         self._log("Trying baudrate: %d" % (baudrate))
458                                                         self._baudrateDetectRetry = 5
459                                                         self._baudrateDetectTestOk = 0
460                                                         timeout = time.time() + 5
461                                                         self._serial.write('\n')
462                                                         self._sendCommand("M105")
463                                                         self._testingBaudrate = True
464                                                 except:
465                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
466                                 elif 'T:' in line:
467                                         self._baudrateDetectTestOk += 1
468                                         if self._baudrateDetectTestOk < 10:
469                                                 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
470                                                 self._sendCommand("M105")
471                                         else:
472                                                 self._sendCommand("M999")
473                                                 self._serial.timeout = 2
474                                                 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
475                                                 self._changeState(self.STATE_OPERATIONAL)
476                                 else:
477                                         self._testingBaudrate = False
478                         elif self._state == self.STATE_CONNECTING:
479                                 if line == '' or 'wait' in line or 'start' in line:        # 'wait' needed for Repetier (kind of watchdog)
480                                         self._sendCommand("M105")
481                                 elif 'ok' in line:
482                                         self._changeState(self.STATE_OPERATIONAL)
483                                 if time.time() > timeout:
484                                         self.close()
485                         elif self._state == self.STATE_OPERATIONAL:
486                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
487                                 if line == '':
488                                         if self._extruderCount > 0:
489                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
490                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
491                                         else:
492                                                 self.sendCommand("M105")
493                                         tempRequestTimeout = time.time() + 5
494                                 elif 'ok' in line:
495                                         self.receivedOK()
496                                 elif 'start' in line:
497                                         self._currentCommands = []
498                         elif self._state == self.STATE_PRINTING:
499                                 #Even when printing request the temperature every 5 seconds.
500                                 if time.time() > tempRequestTimeout:
501                                         if self._extruderCount > 0:
502                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
503                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
504                                         else:
505                                                 self.sendCommand("M105")
506                                         tempRequestTimeout = time.time() + 5
507                                 if line == '' and time.time() > timeout:
508                                         self._log("Communication timeout during printing, forcing a line")
509                                         line = 'ok'
510                                 if 'ok' in line:
511                                         self.receivedOK()
512                                         timeout = time.time() + 5
513                                         if not self._commandQueue.empty():
514                                                 self._sendCommand(self._commandQueue.get())
515                                         else:
516                                                 self._sendNext()
517                                         if "G28" in self._currentCommands[0] or "G29" in self._currentCommands[0] or \
518                                            "M109" in self._currentCommands[0] or "M190" in self._currentCommands[0]:
519                                                 # Long command detected. Timeout is now set to 60s to avoid forcing 'ok'
520                                                 # every 5 seconds while it's not needed
521                                                 timeout = time.time() + 60
522
523                                 elif 'start' in line:
524                                         self._currentCommands = []
525                                 elif "resend" in line.lower() or "rs" in line:
526                                         newPos = self._gcodePos
527                                         try:
528                                                 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
529                                         except:
530                                                 if "rs" in line:
531                                                         newPos = int(line.split()[1])
532                                         # If we need to resend more than 10 lines, we can assume that the machine
533                                         # was shut down and turned back on or something else that's weird just happened.
534                                         # In that case, it can be dangerous to restart the print, so we'd better kill it
535                                         if newPos == 1 or self._gcodePos > newPos + 100:
536                                                 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
537                                                 self.cancelPrint()
538                                         else:
539                                                 self._gcodePos = newPos
540                         elif self._state == self.STATE_PAUSED:
541                                 #Even when printing request the temperature every 5 seconds.
542                                 if time.time() > tempRequestTimeout:
543                                         if self._extruderCount > 0:
544                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
545                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
546                                         else:
547                                                 self.sendCommand("M105")
548                                         tempRequestTimeout = time.time() + 5
549                                 if line == '' and time.time() > timeout:
550                                         line = 'ok'
551                                 if 'ok' in line:
552                                         self.receivedOK()
553                                         timeout = time.time() + 5
554                                         if not self._commandQueue.empty():
555                                                 self._sendCommand(self._commandQueue.get())
556                                 elif 'start' in line:
557                                         self._currentCommands = []
558
559                 self._log("Connection closed, closing down monitor")
560
561         def _setBaudrate(self, baudrate):
562                 try:
563                         self._serial.baudrate = baudrate
564                 except:
565                         print getExceptionString()
566
567         def _log(self, message):
568                 #sys.stderr.write(message + "\n");
569                 self._callback.mcLog(message)
570                 try:
571                         self._logQueue.put(message, False)
572                 except:
573                         #If the log queue is full, remove the first message and append the new message again
574                         self._logQueue.get()
575                         try:
576                                 self._logQueue.put(message, False)
577                         except:
578                                 pass
579
580         def _readline(self):
581                 if self._serial is None:
582                         return None
583                 try:
584                         ret = self._serial.readline()
585                 except:
586                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
587                         self._errorValue = getExceptionString()
588                         self.close(True)
589                         return None
590                 if ret == '':
591                         #self._log("Recv: TIMEOUT")
592                         return ''
593                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
594                 return ret
595         
596         def close(self, isError = False):
597                 if self._serial != None:
598                         self._serial.close()
599                         if isError:
600                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
601                         else:
602                                 self._changeState(self.STATE_CLOSED)
603                 self._serial = None
604         
605         def __del__(self):
606                 self.close()
607         
608         def _sendCommand(self, cmd):
609                 self._thread_lock.acquire(True)
610                 if self._serial is None:
611                         self._thread_lock.release()
612                         return
613                 if 'M109' in cmd or 'M190' in cmd:
614                         self._heatupWaitStartTime = time.time()
615                 if 'M104' in cmd or 'M109' in cmd:
616                         try:
617                                 t = 0
618                                 if 'T' in cmd:
619                                         t = int(re.search('T([0-9]+)', cmd).group(1))
620                                 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
621                         except:
622                                 pass
623                 if 'M140' in cmd or 'M190' in cmd:
624                         try:
625                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
626                         except:
627                                 pass
628                 self._log('Send: %s' % (cmd))
629                 if self.isOperational():
630                         self._currentCommands.append(cmd)
631                 try:
632                         self._serial.write(cmd + '\n')
633                 except serial.SerialTimeoutException:
634                         self._log("Serial timeout while writing to serial port, trying again.")
635                         try:
636                                 time.sleep(0.5)
637                                 self._serial.write(cmd + '\n')
638                         except:
639                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
640                                 self._errorValue = getExceptionString()
641                                 self.close(True)
642                 except:
643                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
644                         self._errorValue = getExceptionString()
645                         self.close(True)
646                 self._thread_lock.release()
647
648         def _sendNext(self):
649                 if self._gcodePos >= len(self._gcodeList):
650                         self._changeState(self.STATE_OPERATIONAL)
651                         return
652                 if self._gcodePos == 100:
653                         self._printStartTime100 = time.time()
654                 line = self._gcodeList[self._gcodePos]
655                 if type(line) is tuple:
656                         self._printSection = line[1]
657                         line = line[0]
658                 try:
659                         if line == 'M0' or line == 'M1':
660                                 self.setPause(True)
661                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
662                         if self._printSection in self._feedRateModifier:
663                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
664                         if ('G0' in line or 'G1' in line) and 'Z' in line:
665                                 z = float(re.search('Z(-?[0-9\.]*)', line).group(1))
666                                 if self._currentZ != z:
667                                         self._currentZ = z
668                                         self._callback.mcZChange(z)
669                 except:
670                         self._log("Unexpected error: %s" % (getExceptionString()))
671                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
672                 pos = self._gcodePos
673                 self._gcodePos += 1
674                 self._sendCommand("N%d%s*%d" % (pos, line, checksum))
675                 self._callback.mcProgress(self._gcodePos)
676
677         def sendCommand(self, cmd):
678                 cmd = cmd.encode('ascii', 'replace')
679                 if self.isPrinting() or self.isPaused():
680                         self._commandQueue.put(cmd)
681                         if len(self._currentCommands) == 0:
682                                 self._sendCommand(self._commandQueue.get())
683                 elif self.isOperational():
684                         self._sendCommand(cmd)
685
686         def printGCode(self, gcodeList):
687                 if not self.isOperational() or self.isPrinting() or self.isPaused():
688                         return
689                 self._gcodeList = gcodeList
690                 self._gcodePos = 0
691                 self._printStartTime100 = None
692                 self._printSection = 'CUSTOM'
693                 self._changeState(self.STATE_PRINTING)
694                 self._printStartTime = time.time()
695                 for i in xrange(0, 4):
696                         self._sendNext()
697         
698         def cancelPrint(self):
699                 if self.isOperational():
700                         self._changeState(self.STATE_OPERATIONAL)
701         
702         def setPause(self, pause):
703                 if not pause and self.isPaused():
704                         self._changeState(self.STATE_PRINTING)
705                         for i in xrange(0, 4):
706                                 if not self._commandQueue.empty():
707                                         self._sendCommand(self._commandQueue.get())
708                                 else:
709                                         self._sendNext()
710                 if pause and self.isPrinting():
711                         self._changeState(self.STATE_PAUSED)
712         
713         def setFeedrateModifier(self, type, value):
714                 self._feedRateModifier[type] = value
715
716 def getExceptionString():
717         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
718         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])