chiark / gitweb /
Merge pull request #601 from CapnBry/reloadscene
[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*")
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 not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and line != 'echo:Unknown command:""\n' and self.isOperational():
399                                 self._callback.mcMessage(line)
400
401                         if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
402                                 if line == '' or time.time() > timeout:
403                                         if len(self._baudrateDetectList) < 1:
404                                                 self.close()
405                                                 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
406                                                 self._changeState(self.STATE_ERROR)
407                                         elif self._baudrateDetectRetry > 0:
408                                                 self._baudrateDetectRetry -= 1
409                                                 self._serial.write('\n')
410                                                 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
411                                                 self._sendCommand("M105")
412                                                 self._testingBaudrate = True
413                                         else:
414                                                 if self._state == self.STATE_DETECT_SERIAL:
415                                                         if len(self._serialDetectList) == 0:
416                                                                 if len(self._baudrateDetectList) == 0:
417                                                                         self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
418                                                                         self._errorValue = 'Failed to autodetect serial port.'
419                                                                         self._changeState(self.STATE_ERROR)
420                                                                         return
421                                                                 else:
422                                                                         self._serialDetectList = serialList(True)
423                                                                         baudrate = self._baudrateDetectList.pop(0)
424                                                         self._serial.close()
425                                                         self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=2.5, writeTimeout=10000)
426                                                 else:
427                                                         baudrate = self._baudrateDetectList.pop(0)
428                                                 try:
429                                                         self._setBaudrate(baudrate)
430                                                         self._serial.timeout = 0.5
431                                                         self._log("Trying baudrate: %d" % (baudrate))
432                                                         self._baudrateDetectRetry = 5
433                                                         self._baudrateDetectTestOk = 0
434                                                         timeout = time.time() + 5
435                                                         self._serial.write('\n')
436                                                         self._sendCommand("M105")
437                                                         self._testingBaudrate = True
438                                                 except:
439                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
440                                 elif 'T:' in line:
441                                         self._baudrateDetectTestOk += 1
442                                         if self._baudrateDetectTestOk < 10:
443                                                 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
444                                                 self._sendCommand("M105")
445                                         else:
446                                                 self._sendCommand("M999")
447                                                 self._serial.timeout = 2
448                                                 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
449                                                 self._changeState(self.STATE_OPERATIONAL)
450                                 else:
451                                         self._testingBaudrate = False
452                         elif self._state == self.STATE_CONNECTING:
453                                 if line == '' or 'wait' in line:        # 'wait' needed for Repetier (kind of watchdog)
454                                         self._sendCommand("M105")
455                                 elif 'ok' in line:
456                                         self._changeState(self.STATE_OPERATIONAL)
457                                 if time.time() > timeout:
458                                         self.close()
459                         elif self._state == self.STATE_OPERATIONAL:
460                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
461                                 if line == '':
462                                         if self._extruderCount > 0:
463                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
464                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
465                                         else:
466                                                 self.sendCommand("M105")
467                                         tempRequestTimeout = time.time() + 5
468                         elif self._state == self.STATE_PRINTING:
469                                 #Even when printing request the temperature every 5 seconds.
470                                 if time.time() > tempRequestTimeout:
471                                         if self._extruderCount > 0:
472                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
473                                                 self.sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
474                                         else:
475                                                 self.sendCommand("M105")
476                                         tempRequestTimeout = time.time() + 5
477                                 if line == '' and time.time() > timeout:
478                                         self._log("Communication timeout during printing, forcing a line")
479                                         line = 'ok'
480                                 if 'ok' in line:
481                                         timeout = time.time() + 5
482                                         if not self._commandQueue.empty():
483                                                 self._sendCommand(self._commandQueue.get())
484                                         else:
485                                                 self._sendNext()
486                                 elif "resend" in line.lower() or "rs" in line:
487                                         try:
488                                                 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
489                                         except:
490                                                 if "rs" in line:
491                                                         self._gcodePos = int(line.split()[1])
492                 self._log("Connection closed, closing down monitor")
493
494         def _setBaudrate(self, baudrate):
495                 #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
496                 if sys.platform.startswith('linux'):
497                         try:
498                                 self._serial.baudrate = baudrate
499                         except:
500                                 try:
501                                         # set custom speed
502                                         import fcntl, array, termios
503                                         TCGETS2 = 0x802C542A
504                                         TCSETS2 = 0x402C542B
505                                         BOTHER = 0o010000
506                                         buf = array.array('i', [0] * 64)
507                                         fcntl.ioctl(self._serial.fd, TCGETS2, buf)
508                                         buf[2] &= ~termios.CBAUD
509                                         buf[2] |= BOTHER
510                                         buf[9] = buf[10] = baudrate
511                                         fcntl.ioctl(self._serial.fd, TCSETS2, buf)
512                                 except:
513                                         print getExceptionString()
514                 else:
515                         self._serial.baudrate = baudrate
516
517         def _log(self, message):
518                 self._callback.mcLog(message)
519                 try:
520                         self._logQueue.put(message, False)
521                 except:
522                         #If the log queue is full, remove the first message and append the new message again
523                         self._logQueue.get()
524                         try:
525                                 self._logQueue.put(message, False)
526                         except:
527                                 pass
528
529         def _readline(self):
530                 if self._serial is None:
531                         return None
532                 try:
533                         ret = self._serial.readline()
534                 except:
535                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
536                         self._errorValue = getExceptionString()
537                         self.close(True)
538                         return None
539                 if ret == '':
540                         #self._log("Recv: TIMEOUT")
541                         return ''
542                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
543                 return ret
544         
545         def close(self, isError = False):
546                 if self._serial != None:
547                         self._serial.close()
548                         if isError:
549                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
550                         else:
551                                 self._changeState(self.STATE_CLOSED)
552                 self._serial = None
553         
554         def __del__(self):
555                 self.close()
556         
557         def _sendCommand(self, cmd):
558                 if self._serial is None:
559                         return
560                 if 'M109' in cmd or 'M190' in cmd:
561                         self._heatupWaitStartTime = time.time()
562                 if 'M104' in cmd or 'M109' in cmd:
563                         try:
564                                 t = 0
565                                 if 'T' in cmd:
566                                         t = int(re.search('T([0-9]+)', cmd).group(1))
567                                 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
568                         except:
569                                 pass
570                 if 'M140' in cmd or 'M190' in cmd:
571                         try:
572                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
573                         except:
574                                 pass
575                 self._log('Send: %s' % (cmd))
576                 try:
577                         self._serial.write(cmd + '\n')
578                 except serial.SerialTimeoutException:
579                         self._log("Serial timeout while writing to serial port, trying again.")
580                         try:
581                                 time.sleep(0.5)
582                                 self._serial.write(cmd + '\n')
583                         except:
584                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
585                                 self._errorValue = getExceptionString()
586                                 self.close(True)
587                 except:
588                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
589                         self._errorValue = getExceptionString()
590                         self.close(True)
591         
592         def _sendNext(self):
593                 if self._gcodePos >= len(self._gcodeList):
594                         self._changeState(self.STATE_OPERATIONAL)
595                         return
596                 if self._gcodePos == 100:
597                         self._printStartTime100 = time.time()
598                 line = self._gcodeList[self._gcodePos]
599                 if type(line) is tuple:
600                         self._printSection = line[1]
601                         line = line[0]
602                 try:
603                         if line == 'M0' or line == 'M1':
604                                 self.setPause(True)
605                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
606                         if self._printSection in self._feedRateModifier:
607                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
608                         if ('G0' in line or 'G1' in line) and 'Z' in line:
609                                 z = float(re.search('Z([0-9\.]*)', line).group(1))
610                                 if self._currentZ != z:
611                                         self._currentZ = z
612                                         self._callback.mcZChange(z)
613                 except:
614                         self._log("Unexpected error: %s" % (getExceptionString()))
615                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
616                 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
617                 self._gcodePos += 1
618                 self._callback.mcProgress(self._gcodePos)
619         
620         def sendCommand(self, cmd):
621                 cmd = cmd.encode('ascii', 'replace')
622                 if self.isPrinting():
623                         self._commandQueue.put(cmd)
624                 elif self.isOperational():
625                         self._sendCommand(cmd)
626         
627         def printGCode(self, gcodeList):
628                 if not self.isOperational() or self.isPrinting():
629                         return
630                 self._gcodeList = gcodeList
631                 self._gcodePos = 0
632                 self._printStartTime100 = None
633                 self._printSection = 'CUSTOM'
634                 self._changeState(self.STATE_PRINTING)
635                 self._printStartTime = time.time()
636                 for i in xrange(0, 4):
637                         self._sendNext()
638         
639         def cancelPrint(self):
640                 if self.isOperational():
641                         self._changeState(self.STATE_OPERATIONAL)
642         
643         def setPause(self, pause):
644                 if not pause and self.isPaused():
645                         self._changeState(self.STATE_PRINTING)
646                         for i in xrange(0, 6):
647                                 self._sendNext()
648                 if pause and self.isPrinting():
649                         self._changeState(self.STATE_PAUSED)
650         
651         def setFeedrateModifier(self, type, value):
652                 self._feedRateModifier[type] = value
653
654 def getExceptionString():
655         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
656         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])