chiark / gitweb /
Do not flood terminal with progress numbers when doing a 'split object into parts'
[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                 
206                 self.thread = threading.Thread(target=self._monitor)
207                 self.thread.daemon = True
208                 self.thread.start()
209         
210         def _changeState(self, newState):
211                 if self._state == newState:
212                         return
213                 oldState = self.getStateString()
214                 self._state = newState
215                 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
216                 self._callback.mcStateChange(newState)
217         
218         def getState(self):
219                 return self._state
220         
221         def getStateString(self):
222                 if self._state == self.STATE_NONE:
223                         return "Offline"
224                 if self._state == self.STATE_OPEN_SERIAL:
225                         return "Opening serial port"
226                 if self._state == self.STATE_DETECT_SERIAL:
227                         return "Detecting serial port"
228                 if self._state == self.STATE_DETECT_BAUDRATE:
229                         return "Detecting baudrate"
230                 if self._state == self.STATE_CONNECTING:
231                         return "Connecting"
232                 if self._state == self.STATE_OPERATIONAL:
233                         return "Operational"
234                 if self._state == self.STATE_PRINTING:
235                         return "Printing"
236                 if self._state == self.STATE_PAUSED:
237                         return "Paused"
238                 if self._state == self.STATE_CLOSED:
239                         return "Closed"
240                 if self._state == self.STATE_ERROR:
241                         return "Error: %s" % (self.getShortErrorString())
242                 if self._state == self.STATE_CLOSED_WITH_ERROR:
243                         return "Error: %s" % (self.getShortErrorString())
244                 return "?%d?" % (self._state)
245         
246         def getShortErrorString(self):
247                 if len(self._errorValue) < 35:
248                         return self._errorValue
249                 return self._errorValue[:35] + "..."
250
251         def getErrorString(self):
252                 return self._errorValue
253
254         def isClosed(self):
255                 return self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
256
257         def isClosedOrError(self):
258                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
259
260         def isError(self):
261                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
262         
263         def isOperational(self):
264                 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
265         
266         def isPrinting(self):
267                 return self._state == self.STATE_PRINTING
268         
269         def isPaused(self):
270                 return self._state == self.STATE_PAUSED
271
272         def getPrintPos(self):
273                 return self._gcodePos
274         
275         def getPrintTime(self):
276                 return time.time() - self._printStartTime
277
278         def getPrintTimeRemainingEstimate(self):
279                 if self._printStartTime100 is None or self.getPrintPos() < 200:
280                         return None
281                 printTime = (time.time() - self._printStartTime100) / 60
282                 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
283                 printTimeLeft = printTimeTotal - printTime
284                 return printTimeLeft
285         
286         def getTemp(self):
287                 return self._temp
288         
289         def getBedTemp(self):
290                 return self._bedTemp
291         
292         def getLog(self):
293                 ret = []
294                 while not self._logQueue.empty():
295                         ret.append(self._logQueue.get())
296                 for line in ret:
297                         self._logQueue.put(line, False)
298                 return ret
299         
300         def _monitor(self):
301                 #Open the serial port.
302                 if self._port == 'AUTO':
303                         self._changeState(self.STATE_DETECT_SERIAL)
304                         programmer = stk500v2.Stk500v2()
305                         for p in serialList(True):
306                                 try:
307                                         self._log("Connecting to: %s (programmer)" % (p))
308                                         programmer.connect(p)
309                                         self._serial = programmer.leaveISP()
310                                         profile.putMachineSetting('serial_port_auto', p)
311                                         break
312                                 except ispBase.IspError as (e):
313                                         self._log("Error while connecting to %s: %s" % (p, str(e)))
314                                         pass
315                                 except:
316                                         self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
317                                 programmer.close()
318                         if self._serial is None:
319                                 self._log("Serial port list: %s" % (str(serialList(True))))
320                                 self._serialDetectList = serialList(True)
321                 elif self._port == 'VIRTUAL':
322                         self._changeState(self.STATE_OPEN_SERIAL)
323                         self._serial = VirtualPrinter()
324                 else:
325                         self._changeState(self.STATE_OPEN_SERIAL)
326                         try:
327                                 if self._baudrate == 0:
328                                         self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
329                                         self._serial = serial.Serial(str(self._port), 115200, timeout=3, writeTimeout=10000)
330                                 else:
331                                         self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
332                                         self._serial = serial.Serial(str(self._port), self._baudrate, timeout=5, writeTimeout=10000)
333                         except:
334                                 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
335                 if self._serial is None:
336                         baudrate = self._baudrate
337                         if baudrate == 0:
338                                 baudrate = self._baudrateDetectList.pop(0)
339                         if len(self._serialDetectList) < 1:
340                                 self._log("Found no ports to try for auto detection")
341                                 self._errorValue = 'Failed to autodetect serial port.'
342                                 self._changeState(self.STATE_ERROR)
343                                 return
344                         port = self._serialDetectList.pop(0)
345                         self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
346                         try:
347                                 self._serial = serial.Serial(port, baudrate, timeout=3, writeTimeout=10000)
348                         except:
349                                 pass
350                 else:
351                         self._log("Connected to: %s, starting monitor" % (self._serial))
352                         if self._baudrate == 0:
353                                 self._changeState(self.STATE_DETECT_BAUDRATE)
354                         else:
355                                 self._changeState(self.STATE_CONNECTING)
356
357                 #Start monitoring the serial port.
358                 if self._state == self.STATE_CONNECTING:
359                         timeout = time.time() + 15
360                 else:
361                         timeout = time.time() + 5
362                 tempRequestTimeout = timeout
363                 while True:
364                         line = self._readline()
365                         if line is None:
366                                 break
367
368                         #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
369                         # Only goto error on known fatal errors.
370                         if line.startswith('Error:'):
371                                 #Oh YEAH, consistency.
372                                 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
373                                 #       But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
374                                 #       So we can have an extra newline in the most common case. Awesome work people.
375                                 if re.match('Error:[0-9]\n', line):
376                                         line = line.rstrip() + self._readline()
377                                 #Skip the communication errors, as those get corrected.
378                                 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:
379                                         if not self.isError():
380                                                 self._errorValue = line[6:]
381                                                 self._changeState(self.STATE_ERROR)
382                         if ' T:' in line or line.startswith('T:'):
383                                 try:
384                                         self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
385                                 except:
386                                         pass
387                                 if 'B:' in line:
388                                         try:
389                                                 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
390                                         except:
391                                                 pass
392                                 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
393                                 #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.
394                                 if not 'ok' in line and self._heatupWaitStartTime != 0:
395                                         t = time.time()
396                                         self._heatupWaitTimeLost = t - self._heatupWaitStartTime
397                                         self._heatupWaitStartTime = t
398                         elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and \
399                                  not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and \
400                                  not line.startswith('Error:No Checksum with line number') and not line.startswith('Error:No Line Number with checksum') and \
401                                  line != 'echo:Unknown command:""\n' and self.isOperational():
402                                 self._callback.mcMessage(line)
403
404                         if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
405                                 if line == '' or time.time() > timeout:
406                                         if len(self._baudrateDetectList) < 1:
407                                                 self.close()
408                                                 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
409                                                 self._changeState(self.STATE_ERROR)
410                                         elif self._baudrateDetectRetry > 0:
411                                                 self._baudrateDetectRetry -= 1
412                                                 self._serial.write('\n')
413                                                 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
414                                                 self._sendCommand("M105")
415                                                 self._testingBaudrate = True
416                                         else:
417                                                 if self._state == self.STATE_DETECT_SERIAL:
418                                                         if len(self._serialDetectList) == 0:
419                                                                 if len(self._baudrateDetectList) == 0:
420                                                                         self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
421                                                                         self._errorValue = 'Failed to autodetect serial port.'
422                                                                         self._changeState(self.STATE_ERROR)
423                                                                         return
424                                                                 else:
425                                                                         self._serialDetectList = serialList(True)
426                                                                         baudrate = self._baudrateDetectList.pop(0)
427                                                         self._serial.close()
428                                                         self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
429                                                 else:
430                                                         baudrate = self._baudrateDetectList.pop(0)
431                                                 try:
432                                                         self._setBaudrate(baudrate)
433                                                         self._serial.timeout = 0.5
434                                                         self._log("Trying baudrate: %d" % (baudrate))
435                                                         self._baudrateDetectRetry = 5
436                                                         self._baudrateDetectTestOk = 0
437                                                         timeout = time.time() + 5
438                                                         self._serial.write('\n')
439                                                         self._sendCommand("M105")
440                                                         self._testingBaudrate = True
441                                                 except:
442                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
443                                 elif 'T:' in line:
444                                         self._baudrateDetectTestOk += 1
445                                         if self._baudrateDetectTestOk < 10:
446                                                 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
447                                                 self._sendCommand("M105")
448                                         else:
449                                                 self._sendCommand("M999")
450                                                 self._serial.timeout = 2
451                                                 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
452                                                 self._changeState(self.STATE_OPERATIONAL)
453                                 else:
454                                         self._testingBaudrate = False
455                         elif self._state == self.STATE_CONNECTING:
456                                 if line == '' or 'wait' in line:        # 'wait' needed for Repetier (kind of watchdog)
457                                         self._sendCommand("M105")
458                                 elif 'ok' in line:
459                                         self._changeState(self.STATE_OPERATIONAL)
460                                 if time.time() > timeout:
461                                         self.close()
462                         elif self._state == self.STATE_OPERATIONAL:
463                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
464                                 if line == '':
465                                         if self._extruderCount > 0:
466                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
467                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
468                                         else:
469                                                 self.sendCommand("M105")
470                                         tempRequestTimeout = time.time() + 5
471                         elif self._state == self.STATE_PRINTING:
472                                 #Even when printing request the temperature every 5 seconds.
473                                 if time.time() > tempRequestTimeout:
474                                         if self._extruderCount > 0:
475                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
476                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
477                                         else:
478                                                 self.sendCommand("M105")
479                                         tempRequestTimeout = time.time() + 5
480                                 if line == '' and time.time() > timeout:
481                                         self._log("Communication timeout during printing, forcing a line")
482                                         line = 'ok'
483                                 if 'ok' in line:
484                                         timeout = time.time() + 5
485                                         if not self._commandQueue.empty():
486                                                 self._sendCommand(self._commandQueue.get())
487                                         else:
488                                                 self._sendNext()
489                                 elif "resend" in line.lower() or "rs" in line:
490                                         newPos = self._gcodePos
491                                         try:
492                                                 newPos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
493                                         except:
494                                                 if "rs" in line:
495                                                         newPos = int(line.split()[1])
496                                         # If we need to resend more than 10 lines, we can assume that the machine
497                                         # was shut down and turned back on or something else that's weird just happened.
498                                         # In that case, it can be dangerous to restart the print, so we'd better kill it
499                                         if newPos == 1 or self._gcodePos > newPos + 100:
500                                                 self._callback.mcMessage("Print canceled due to loss of communication to printer (USB unplugged or power lost)")
501                                                 self.cancelPrint()
502                                         else:
503                                                 self._gcodePos = newPos
504
505                 self._log("Connection closed, closing down monitor")
506
507         def _setBaudrate(self, baudrate):
508                 try:
509                         self._serial.baudrate = baudrate
510                 except:
511                         print getExceptionString()
512
513         def _log(self, message):
514                 self._callback.mcLog(message)
515                 try:
516                         self._logQueue.put(message, False)
517                 except:
518                         #If the log queue is full, remove the first message and append the new message again
519                         self._logQueue.get()
520                         try:
521                                 self._logQueue.put(message, False)
522                         except:
523                                 pass
524
525         def _readline(self):
526                 if self._serial is None:
527                         return None
528                 try:
529                         ret = self._serial.readline()
530                 except:
531                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
532                         self._errorValue = getExceptionString()
533                         self.close(True)
534                         return None
535                 if ret == '':
536                         #self._log("Recv: TIMEOUT")
537                         return ''
538                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
539                 return ret
540         
541         def close(self, isError = False):
542                 if self._serial != None:
543                         self._serial.close()
544                         if isError:
545                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
546                         else:
547                                 self._changeState(self.STATE_CLOSED)
548                 self._serial = None
549         
550         def __del__(self):
551                 self.close()
552         
553         def _sendCommand(self, cmd):
554                 if self._serial is None:
555                         return
556                 if 'M109' in cmd or 'M190' in cmd:
557                         self._heatupWaitStartTime = time.time()
558                 if 'M104' in cmd or 'M109' in cmd:
559                         try:
560                                 t = 0
561                                 if 'T' in cmd:
562                                         t = int(re.search('T([0-9]+)', cmd).group(1))
563                                 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
564                         except:
565                                 pass
566                 if 'M140' in cmd or 'M190' in cmd:
567                         try:
568                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
569                         except:
570                                 pass
571                 self._log('Send: %s' % (cmd))
572                 try:
573                         self._serial.write(cmd + '\n')
574                 except serial.SerialTimeoutException:
575                         self._log("Serial timeout while writing to serial port, trying again.")
576                         try:
577                                 time.sleep(0.5)
578                                 self._serial.write(cmd + '\n')
579                         except:
580                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
581                                 self._errorValue = getExceptionString()
582                                 self.close(True)
583                 except:
584                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
585                         self._errorValue = getExceptionString()
586                         self.close(True)
587         
588         def _sendNext(self):
589                 if self._gcodePos >= len(self._gcodeList):
590                         self._changeState(self.STATE_OPERATIONAL)
591                         return
592                 if self._gcodePos == 100:
593                         self._printStartTime100 = time.time()
594                 line = self._gcodeList[self._gcodePos]
595                 if type(line) is tuple:
596                         self._printSection = line[1]
597                         line = line[0]
598                 try:
599                         if line == 'M0' or line == 'M1':
600                                 self.setPause(True)
601                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
602                         if self._printSection in self._feedRateModifier:
603                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
604                         if ('G0' in line or 'G1' in line) and 'Z' in line:
605                                 z = float(re.search('Z([0-9\.]*)', line).group(1))
606                                 if self._currentZ != z:
607                                         self._currentZ = z
608                                         self._callback.mcZChange(z)
609                 except:
610                         self._log("Unexpected error: %s" % (getExceptionString()))
611                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
612                 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
613                 self._gcodePos += 1
614                 self._callback.mcProgress(self._gcodePos)
615         
616         def sendCommand(self, cmd):
617                 cmd = cmd.encode('ascii', 'replace')
618                 if self.isPrinting():
619                         self._commandQueue.put(cmd)
620                 elif self.isOperational():
621                         self._sendCommand(cmd)
622         
623         def printGCode(self, gcodeList):
624                 if not self.isOperational() or self.isPrinting():
625                         return
626                 self._gcodeList = gcodeList
627                 self._gcodePos = 0
628                 self._printStartTime100 = None
629                 self._printSection = 'CUSTOM'
630                 self._changeState(self.STATE_PRINTING)
631                 self._printStartTime = time.time()
632                 for i in xrange(0, 2):
633                         self._sendNext()
634         
635         def cancelPrint(self):
636                 if self.isOperational():
637                         self._changeState(self.STATE_OPERATIONAL)
638         
639         def setPause(self, pause):
640                 if not pause and self.isPaused():
641                         self._changeState(self.STATE_PRINTING)
642                         for i in xrange(0, 2):
643                                 self._sendNext()
644                 if pause and self.isPrinting():
645                         self._changeState(self.STATE_PAUSED)
646         
647         def setFeedrateModifier(self, type, value):
648                 self._feedRateModifier[type] = value
649
650 def getExceptionString():
651         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
652         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])