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