chiark / gitweb /
Merge branch 'upstream'
[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                                         try:
491                                                 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
492                                         except:
493                                                 if "rs" in line:
494                                                         self._gcodePos = int(line.split()[1])
495                 self._log("Connection closed, closing down monitor")
496
497         def _setBaudrate(self, baudrate):
498                 try:
499                         self._serial.baudrate = baudrate
500                 except:
501                         print getExceptionString()
502
503         def _log(self, message):
504                 self._callback.mcLog(message)
505                 try:
506                         self._logQueue.put(message, False)
507                 except:
508                         #If the log queue is full, remove the first message and append the new message again
509                         self._logQueue.get()
510                         try:
511                                 self._logQueue.put(message, False)
512                         except:
513                                 pass
514
515         def _readline(self):
516                 if self._serial is None:
517                         return None
518                 try:
519                         ret = self._serial.readline()
520                 except:
521                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
522                         self._errorValue = getExceptionString()
523                         self.close(True)
524                         return None
525                 if ret == '':
526                         #self._log("Recv: TIMEOUT")
527                         return ''
528                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
529                 return ret
530         
531         def close(self, isError = False):
532                 if self._serial != None:
533                         self._serial.close()
534                         if isError:
535                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
536                         else:
537                                 self._changeState(self.STATE_CLOSED)
538                 self._serial = None
539         
540         def __del__(self):
541                 self.close()
542         
543         def _sendCommand(self, cmd):
544                 if self._serial is None:
545                         return
546                 if 'M109' in cmd or 'M190' in cmd:
547                         self._heatupWaitStartTime = time.time()
548                 if 'M104' in cmd or 'M109' in cmd:
549                         try:
550                                 t = 0
551                                 if 'T' in cmd:
552                                         t = int(re.search('T([0-9]+)', cmd).group(1))
553                                 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
554                         except:
555                                 pass
556                 if 'M140' in cmd or 'M190' in cmd:
557                         try:
558                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
559                         except:
560                                 pass
561                 self._log('Send: %s' % (cmd))
562                 try:
563                         self._serial.write(cmd + '\n')
564                 except serial.SerialTimeoutException:
565                         self._log("Serial timeout while writing to serial port, trying again.")
566                         try:
567                                 time.sleep(0.5)
568                                 self._serial.write(cmd + '\n')
569                         except:
570                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
571                                 self._errorValue = getExceptionString()
572                                 self.close(True)
573                 except:
574                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
575                         self._errorValue = getExceptionString()
576                         self.close(True)
577         
578         def _sendNext(self):
579                 if self._gcodePos >= len(self._gcodeList):
580                         self._changeState(self.STATE_OPERATIONAL)
581                         return
582                 if self._gcodePos == 100:
583                         self._printStartTime100 = time.time()
584                 line = self._gcodeList[self._gcodePos]
585                 if type(line) is tuple:
586                         self._printSection = line[1]
587                         line = line[0]
588                 try:
589                         if line == 'M0' or line == 'M1':
590                                 self.setPause(True)
591                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
592                         if self._printSection in self._feedRateModifier:
593                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
594                         if ('G0' in line or 'G1' in line) and 'Z' in line:
595                                 z = float(re.search('Z([0-9\.]*)', line).group(1))
596                                 if self._currentZ != z:
597                                         self._currentZ = z
598                                         self._callback.mcZChange(z)
599                 except:
600                         self._log("Unexpected error: %s" % (getExceptionString()))
601                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
602                 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
603                 self._gcodePos += 1
604                 self._callback.mcProgress(self._gcodePos)
605         
606         def sendCommand(self, cmd):
607                 cmd = cmd.encode('ascii', 'replace')
608                 if self.isPrinting():
609                         self._commandQueue.put(cmd)
610                 elif self.isOperational():
611                         self._sendCommand(cmd)
612         
613         def printGCode(self, gcodeList):
614                 if not self.isOperational() or self.isPrinting():
615                         return
616                 self._gcodeList = gcodeList
617                 self._gcodePos = 0
618                 self._printStartTime100 = None
619                 self._printSection = 'CUSTOM'
620                 self._changeState(self.STATE_PRINTING)
621                 self._printStartTime = time.time()
622                 for i in xrange(0, 2):
623                         self._sendNext()
624         
625         def cancelPrint(self):
626                 if self.isOperational():
627                         self._changeState(self.STATE_OPERATIONAL)
628         
629         def setPause(self, pause):
630                 if not pause and self.isPaused():
631                         self._changeState(self.STATE_PRINTING)
632                         for i in xrange(0, 2):
633                                 self._sendNext()
634                 if pause and self.isPrinting():
635                         self._changeState(self.STATE_PAUSED)
636         
637         def setFeedrateModifier(self, type, value):
638                 self._feedRateModifier[type] = value
639
640 def getExceptionString():
641         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
642         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])