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