chiark / gitweb /
show baudrates while connecting
[cura.git] / Cura / util / machineCom.py
1 from __future__ import absolute_import
2 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
3
4 import os
5 import glob
6 import sys
7 import time
8 import math
9 import re
10 import traceback
11 import threading
12 import platform
13 import Queue as queue
14
15 import serial
16
17 from Cura.avr_isp import stk500v2
18 from Cura.avr_isp import ispBase
19
20 from Cura.util import profile
21 from Cura.util import version
22
23 try:
24         import _winreg
25 except:
26         pass
27
28 def serialList(forAutoDetect=False):
29         baselist=[]
30         if platform.system() == "Windows":
31                 try:
32                         key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
33                         i=0
34                         while True:
35                                 values = _winreg.EnumValue(key, i)
36                                 if not forAutoDetect or 'USBSER' in values[0]:
37                                         baselist+=[values[1]]
38                                 i+=1
39                 except:
40                         pass
41         if forAutoDetect:
42                 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.usb*")
43                 baselist = filter(lambda s: not 'Bluetooth' in s, baselist)
44                 prev = profile.getMachineSetting('serial_port_auto')
45                 if prev in baselist:
46                         baselist.remove(prev)
47                         baselist.insert(0, prev)
48         else:
49                 baselist = baselist + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*")
50         if version.isDevVersion() and not forAutoDetect:
51                 baselist.append('VIRTUAL')
52         return baselist
53
54 def machineIsConnected():
55         #UltiGCode is designed for SD-Card printing, so never auto-detect the serial port.
56         port = profile.getMachineSetting('serial_port')
57         if port == 'AUTO':
58                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
59                         return False
60                 return len(serialList(True)) > 0
61         if platform.system() == "Windows":
62                 return port in serialList()
63         return os.path.isfile(port)
64
65 def baudrateList():
66         ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
67         if profile.getMachineSetting('serial_baud_auto') != '':
68                 prev = int(profile.getMachineSetting('serial_baud_auto'))
69                 if prev in ret:
70                         ret.remove(prev)
71                         ret.insert(0, prev)
72         return ret
73
74 class VirtualPrinter():
75         def __init__(self):
76                 self.readList = ['start\n', 'Marlin: Virtual Marlin!\n', '\x80\n']
77                 self.temp = 0.0
78                 self.targetTemp = 0.0
79                 self.lastTempAt = time.time()
80                 self.bedTemp = 1.0
81                 self.bedTargetTemp = 1.0
82         
83         def write(self, data):
84                 if self.readList is None:
85                         return
86                 #print "Send: %s" % (data.rstrip())
87                 if 'M104' in data or 'M109' in data:
88                         try:
89                                 self.targetTemp = float(re.search('S([0-9]+)', data).group(1))
90                         except:
91                                 pass
92                 if 'M140' in data or 'M190' in data:
93                         try:
94                                 self.bedTargetTemp = float(re.search('S([0-9]+)', data).group(1))
95                         except:
96                                 pass
97                 if 'M105' in data:
98                         self.readList.append("ok T:%.2f /%.2f B:%.2f /%.2f @:64\n" % (self.temp, self.targetTemp, self.bedTemp, self.bedTargetTemp))
99                 elif len(data.strip()) > 0:
100                         self.readList.append("ok\n")
101
102         def readline(self):
103                 if self.readList is None:
104                         return ''
105                 n = 0
106                 timeDiff = self.lastTempAt - time.time()
107                 self.lastTempAt = time.time()
108                 if abs(self.temp - self.targetTemp) > 1:
109                         self.temp += math.copysign(timeDiff * 10, self.targetTemp - self.temp)
110                 if abs(self.bedTemp - self.bedTargetTemp) > 1:
111                         self.bedTemp += math.copysign(timeDiff * 10, self.bedTargetTemp - self.bedTemp)
112                 while len(self.readList) < 1:
113                         time.sleep(0.1)
114                         n += 1
115                         if n == 20:
116                                 return ''
117                         if self.readList is None:
118                                 return ''
119                 time.sleep(0.001)
120                 #print "Recv: %s" % (self.readList[0].rstrip())
121                 return self.readList.pop(0)
122         
123         def close(self):
124                 self.readList = None
125
126 class MachineComPrintCallback(object):
127         def mcLog(self, message):
128                 pass
129         
130         def mcTempUpdate(self, temp, bedTemp, targetTemp, bedTargetTemp):
131                 pass
132         
133         def mcStateChange(self, state):
134                 pass
135         
136         def mcMessage(self, message):
137                 pass
138         
139         def mcProgress(self, lineNr):
140                 pass
141         
142         def mcZChange(self, newZ):
143                 pass
144
145 class MachineCom(object):
146         STATE_NONE = 0
147         STATE_OPEN_SERIAL = 1
148         STATE_DETECT_SERIAL = 2
149         STATE_DETECT_BAUDRATE = 3
150         STATE_CONNECTING = 4
151         STATE_OPERATIONAL = 5
152         STATE_PRINTING = 6
153         STATE_PAUSED = 7
154         STATE_CLOSED = 8
155         STATE_ERROR = 9
156         STATE_CLOSED_WITH_ERROR = 10
157         
158         def __init__(self, port = None, baudrate = None, callbackObject = None):
159                 if port is None:
160                         port = profile.getMachineSetting('serial_port')
161                 if baudrate is None:
162                         if profile.getMachineSetting('serial_baud') == 'AUTO':
163                                 baudrate = 0
164                         else:
165                                 baudrate = int(profile.getMachineSetting('serial_baud'))
166                 if callbackObject is None:
167                         callbackObject = MachineComPrintCallback()
168
169                 self._port = port
170                 self._baudrate = baudrate
171                 self._callback = callbackObject
172                 self._state = self.STATE_NONE
173                 self._serial = None
174                 self._serialDetectList = []
175                 self._baudrateDetectList = baudrateList()
176                 self._baudrateDetectRetry = 0
177                 self._extruderCount = int(profile.getMachineSetting('extruder_amount'))
178                 self._temperatureRequestExtruder = 0
179                 self._temp = [0] * self._extruderCount
180                 self._targetTemp = [0] * self._extruderCount
181                 self._bedTemp = 0
182                 self._bedTargetTemp = 0
183                 self._gcodeList = None
184                 self._gcodePos = 0
185                 self._commandQueue = queue.Queue()
186                 self._logQueue = queue.Queue(256)
187                 self._feedRateModifier = {}
188                 self._currentZ = -1
189                 self._heatupWaitStartTime = 0
190                 self._heatupWaitTimeLost = 0.0
191                 self._printStartTime100 = None
192                 
193                 self.thread = threading.Thread(target=self._monitor)
194                 self.thread.daemon = True
195                 self.thread.start()
196         
197         def _changeState(self, newState):
198                 if self._state == newState:
199                         return
200                 oldState = self.getStateString()
201                 self._state = newState
202                 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
203                 self._callback.mcStateChange(newState)
204         
205         def getState(self):
206                 return self._state
207         
208         def getStateString(self):
209                 if self._state == self.STATE_NONE:
210                         return "Offline"
211                 if self._state == self.STATE_OPEN_SERIAL:
212                         return "Opening serial port"
213                 if self._state == self.STATE_DETECT_SERIAL:
214                         return "Detecting serial port"
215                 if self._state == self.STATE_DETECT_BAUDRATE:
216                         return "Detecting baudrate"
217                 if self._state == self.STATE_CONNECTING:
218                         return "Connecting"
219                 if self._state == self.STATE_OPERATIONAL:
220                         return "Operational"
221                 if self._state == self.STATE_PRINTING:
222                         return "Printing"
223                 if self._state == self.STATE_PAUSED:
224                         return "Paused"
225                 if self._state == self.STATE_CLOSED:
226                         return "Closed"
227                 if self._state == self.STATE_ERROR:
228                         return "Error: %s" % (self.getShortErrorString())
229                 if self._state == self.STATE_CLOSED_WITH_ERROR:
230                         return "Error: %s" % (self.getShortErrorString())
231                 return "?%d?" % (self._state)
232         
233         def getShortErrorString(self):
234                 if len(self._errorValue) < 30:
235                         return self._errorValue
236                 return self._errorValue[:30] + "..."
237
238         def getErrorString(self):
239                 return self._errorValue
240         
241         def isClosedOrError(self):
242                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
243
244         def isError(self):
245                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
246         
247         def isOperational(self):
248                 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
249         
250         def isPrinting(self):
251                 return self._state == self.STATE_PRINTING
252         
253         def isPaused(self):
254                 return self._state == self.STATE_PAUSED
255
256         def getPrintPos(self):
257                 return self._gcodePos
258         
259         def getPrintTime(self):
260                 return time.time() - self._printStartTime
261
262         def getPrintTimeRemainingEstimate(self):
263                 if self._printStartTime100 is None or self.getPrintPos() < 200:
264                         return None
265                 printTime = (time.time() - self._printStartTime100) / 60
266                 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
267                 printTimeLeft = printTimeTotal - printTime
268                 return printTimeLeft
269         
270         def getTemp(self):
271                 return self._temp
272         
273         def getBedTemp(self):
274                 return self._bedTemp
275         
276         def getLog(self):
277                 ret = []
278                 while not self._logQueue.empty():
279                         ret.append(self._logQueue.get())
280                 for line in ret:
281                         self._logQueue.put(line, False)
282                 return ret
283         
284         def _monitor(self):
285                 #Open the serial port.
286                 if self._port == 'AUTO':
287                         self._changeState(self.STATE_DETECT_SERIAL)
288                         programmer = stk500v2.Stk500v2()
289                         for p in serialList(True):
290                                 try:
291                                         self._log("Connecting to: %s (programmer)" % (p))
292                                         programmer.connect(p)
293                                         self._serial = programmer.leaveISP()
294                                         profile.putMachineSetting('serial_port_auto', p)
295                                         break
296                                 except ispBase.IspError as (e):
297                                         self._log("Error while connecting to %s: %s" % (p, str(e)))
298                                         pass
299                                 except:
300                                         self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
301                                 programmer.close()
302                         if self._serial is None:
303                                 self._log("Serial port list: %s" % (str(serialList(True))))
304                                 self._serialDetectList = serialList(True)
305                 elif self._port == 'VIRTUAL':
306                         self._changeState(self.STATE_OPEN_SERIAL)
307                         self._serial = VirtualPrinter()
308                 else:
309                         self._changeState(self.STATE_OPEN_SERIAL)
310                         try:
311                                 if self._baudrate == 0:
312                                         self._log("Connecting to: %s with baudrate: 115200 (fallback)" % (self._port))
313                                         self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
314                                 else:
315                                         self._log("Connecting to: %s with baudrate: %s (configured)" % (self._port, self._baudrate))
316                                         self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
317                         except:
318                                 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
319                 if self._serial is None:
320                         baudrate = self._baudrate
321                         if baudrate == 0:
322                                 baudrate = self._baudrateDetectList.pop(0)
323                         port = self._serialDetectList.pop(0)
324                         self._log("Connecting to: %s with baudrate: %s (auto)" % (port, baudrate))
325                         try:
326                                 self._serial = serial.Serial(port, baudrate, timeout=0.1, writeTimeout=10000)
327                         except:
328                                 pass
329                 else:
330                         self._log("Connected to: %s, starting monitor" % (self._serial))
331                         if self._baudrate == 0:
332                                 self._changeState(self.STATE_DETECT_BAUDRATE)
333                         else:
334                                 self._changeState(self.STATE_CONNECTING)
335
336                 #Start monitoring the serial port.
337                 if self._state == self.STATE_CONNECTING:
338                         timeout = time.time() + 15
339                 else:
340                         timeout = time.time() + 5
341                 tempRequestTimeout = timeout
342                 while True:
343                         line = self._readline()
344                         if line is None:
345                                 break
346                         
347                         #No matter the state, if we see an fatal error, goto the error state and store the error for reference.
348                         # Only goto error on known fatal errors.
349                         if line.startswith('Error:'):
350                                 #Oh YEAH, consistency.
351                                 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
352                                 #       But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
353                                 #       So we can have an extra newline in the most common case. Awesome work people.
354                                 if re.match('Error:[0-9]\n', line):
355                                         line = line.rstrip() + self._readline()
356                                 #Skip the communication errors, as those get corrected.
357                                 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:
358                                         if not self.isError():
359                                                 self._errorValue = line[6:]
360                                                 self._changeState(self.STATE_ERROR)
361                         if ' T:' in line or line.startswith('T:'):
362                                 try:
363                                         self._temp[self._temperatureRequestExtruder] = float(re.search("T: *([0-9\.]*)", line).group(1))
364                                 except:
365                                         pass
366                                 if 'B:' in line:
367                                         try:
368                                                 self._bedTemp = float(re.search("B: *([0-9\.]*)", line).group(1))
369                                         except:
370                                                 pass
371                                 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
372                                 #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.
373                                 if not 'ok' in line and self._heatupWaitStartTime != 0:
374                                         t = time.time()
375                                         self._heatupWaitTimeLost = t - self._heatupWaitStartTime
376                                         self._heatupWaitStartTime = t
377                         elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and not line.startswith('Error:checksum mismatch') and not line.startswith('Error:Line Number is not Last Line Number+1') and line != 'echo:Unknown command:""\n' and self.isOperational():
378                                 self._callback.mcMessage(line)
379
380                         if self._state == self.STATE_DETECT_BAUDRATE or self._state == self.STATE_DETECT_SERIAL:
381                                 if line == '' or time.time() > timeout:
382                                         if len(self._baudrateDetectList) < 1:
383                                                 self.close()
384                                                 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
385                                                 self._changeState(self.STATE_ERROR)
386                                         elif self._baudrateDetectRetry > 0:
387                                                 self._baudrateDetectRetry -= 1
388                                                 self._serial.write('\n')
389                                                 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
390                                                 self._sendCommand("M105")
391                                                 self._testingBaudrate = True
392                                         else:
393                                                 if self._state == self.STATE_DETECT_SERIAL:
394                                                         if len(self._serialDetectList) == 0:
395                                                                 if len(self._baudrateDetectList) == 0:
396                                                                         self._log("Tried all serial ports and baudrates, but still not printer found that responds to M105.")
397                                                                         self._errorValue = 'Failed to autodetect serial port.'
398                                                                         self._changeState(self.STATE_ERROR)
399                                                                         return
400                                                                 else:
401                                                                         self._serialDetectList = serialList(True)
402                                                                         baudrate = self._baudrateDetectList.pop(0)
403                                                         self._serial.close()
404                                                         self._serial = serial.Serial(self._serialDetectList.pop(0), baudrate, timeout=0.5, writeTimeout=10000)
405                                                 else:
406                                                         baudrate = self._baudrateDetectList.pop(0)
407                                                 try:
408                                                         self._setBaudrate(baudrate)
409                                                         self._serial.timeout = 0.5
410                                                         self._log("Trying baudrate: %d" % (baudrate))
411                                                         self._baudrateDetectRetry = 5
412                                                         self._baudrateDetectTestOk = 0
413                                                         timeout = time.time() + 5
414                                                         self._serial.write('\n')
415                                                         self._sendCommand("M105")
416                                                         self._testingBaudrate = True
417                                                 except:
418                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
419                                 elif 'T:' in line:
420                                         self._baudrateDetectTestOk += 1
421                                         if self._baudrateDetectTestOk < 10:
422                                                 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
423                                                 self._sendCommand("M105")
424                                         else:
425                                                 self._sendCommand("M999")
426                                                 self._serial.timeout = 2
427                                                 profile.putMachineSetting('serial_baud_auto', self._serial.baudrate)
428                                                 self._changeState(self.STATE_OPERATIONAL)
429                                 else:
430                                         self._testingBaudrate = False
431                         elif self._state == self.STATE_CONNECTING:
432                                 if line == '' or 'wait' in line:        # 'wait' needed for Repetier (kind of watchdog)
433                                         self._sendCommand("M105")
434                                 elif 'ok' in line:
435                                         self._changeState(self.STATE_OPERATIONAL)
436                                 if time.time() > timeout:
437                                         self.close()
438                         elif self._state == self.STATE_OPERATIONAL:
439                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
440                                 if line == '':
441                                         if self._extruderCount > 0:
442                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
443                                                 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
444                                         else:
445                                                 self._sendCommand("M105")
446                                         tempRequestTimeout = time.time() + 5
447                         elif self._state == self.STATE_PRINTING:
448                                 if line == '' and time.time() > timeout:
449                                         self._log("Communication timeout during printing, forcing a line")
450                                         line = 'ok'
451                                 #Even when printing request the temperature every 5 seconds.
452                                 if time.time() > tempRequestTimeout:
453                                         if self._extruderCount > 0:
454                                                 self._temperatureRequestExtruder = (self._temperatureRequestExtruder + 1) % self._extruderCount
455                                                 self._sendCommand("M105 T%d" % (self._temperatureRequestExtruder))
456                                         else:
457                                                 self._sendCommand("M105")
458                                         tempRequestTimeout = time.time() + 5
459                                 if 'ok' in line:
460                                         timeout = time.time() + 5
461                                         if not self._commandQueue.empty():
462                                                 self._sendCommand(self._commandQueue.get())
463                                         else:
464                                                 self._sendNext()
465                                 elif "resend" in line.lower() or "rs" in line:
466                                         try:
467                                                 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
468                                         except:
469                                                 if "rs" in line:
470                                                         self._gcodePos = int(line.split()[1])
471                 self._log("Connection closed, closing down monitor")
472
473         def _setBaudrate(self, baudrate):
474                 #For linux the pyserial implementation lacks TCGETS2 support. So do that ourselves
475                 if sys.platform.startswith('linux'):
476                         try:
477                                 self._serial.baudrate = baudrate
478                         except:
479                                 try:
480                                         # set custom speed
481                                         import fcntl, array, termios
482                                         TCGETS2 = 0x802C542A
483                                         TCSETS2 = 0x402C542B
484                                         BOTHER = 0o010000
485                                         buf = array.array('i', [0] * 64)
486                                         fcntl.ioctl(self._serial.fd, TCGETS2, buf)
487                                         buf[2] &= ~termios.CBAUD
488                                         buf[2] |= BOTHER
489                                         buf[9] = buf[10] = baudrate
490                                         fcntl.ioctl(self._serial.fd, TCSETS2, buf)
491                                 except:
492                                         print getExceptionString()
493                 else:
494                         self._serial.baudrate = baudrate
495
496         def _log(self, message):
497                 self._callback.mcLog(message)
498                 try:
499                         self._logQueue.put(message, False)
500                 except:
501                         #If the log queue is full, remove the first message and append the new message again
502                         self._logQueue.get()
503                         try:
504                                 self._logQueue.put(message, False)
505                         except:
506                                 pass
507
508         def _readline(self):
509                 if self._serial == None:
510                         return None
511                 try:
512                         ret = self._serial.readline()
513                 except:
514                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
515                         self._errorValue = getExceptionString()
516                         self.close(True)
517                         return None
518                 if ret == '':
519                         #self._log("Recv: TIMEOUT")
520                         return ''
521                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
522                 return ret
523         
524         def close(self, isError = False):
525                 if self._serial != None:
526                         self._serial.close()
527                         if isError:
528                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
529                         else:
530                                 self._changeState(self.STATE_CLOSED)
531                 self._serial = None
532         
533         def __del__(self):
534                 self.close()
535         
536         def _sendCommand(self, cmd):
537                 if self._serial is None:
538                         return
539                 if 'M109' in cmd or 'M190' in cmd:
540                         self._heatupWaitStartTime = time.time()
541                 if 'M104' in cmd or 'M109' in cmd:
542                         try:
543                                 t = 0
544                                 if 'T' in cmd:
545                                         t = int(re.search('T([0-9]+)', cmd).group(1))
546                                 self._targetTemp[t] = float(re.search('S([0-9]+)', cmd).group(1))
547                         except:
548                                 pass
549                 if 'M140' in cmd or 'M190' in cmd:
550                         try:
551                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
552                         except:
553                                 pass
554                 self._log('Send: %s' % (cmd))
555                 try:
556                         self._serial.write(cmd + '\n')
557                 except serial.SerialTimeoutException:
558                         self._log("Serial timeout while writing to serial port, trying again.")
559                         try:
560                                 time.sleep(0.5)
561                                 self._serial.write(cmd + '\n')
562                         except:
563                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
564                                 self._errorValue = getExceptionString()
565                                 self.close(True)
566                 except:
567                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
568                         self._errorValue = getExceptionString()
569                         self.close(True)
570         
571         def _sendNext(self):
572                 if self._gcodePos >= len(self._gcodeList):
573                         self._changeState(self.STATE_OPERATIONAL)
574                         return
575                 if self._gcodePos == 100:
576                         self._printStartTime100 = time.time()
577                 line = self._gcodeList[self._gcodePos]
578                 if type(line) is tuple:
579                         self._printSection = line[1]
580                         line = line[0]
581                 try:
582                         if line == 'M0' or line == 'M1':
583                                 self.setPause(True)
584                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
585                         if self._printSection in self._feedRateModifier:
586                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
587                         if ('G0' in line or 'G1' in line) and 'Z' in line:
588                                 z = float(re.search('Z([0-9\.]*)', line).group(1))
589                                 if self._currentZ != z:
590                                         self._currentZ = z
591                                         self._callback.mcZChange(z)
592                 except:
593                         self._log("Unexpected error: %s" % (getExceptionString()))
594                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
595                 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
596                 self._gcodePos += 1
597                 self._callback.mcProgress(self._gcodePos)
598         
599         def sendCommand(self, cmd):
600                 cmd = cmd.encode('ascii', 'replace')
601                 if self.isPrinting():
602                         self._commandQueue.put(cmd)
603                 elif self.isOperational():
604                         self._sendCommand(cmd)
605         
606         def printGCode(self, gcodeList):
607                 if not self.isOperational() or self.isPrinting():
608                         return
609                 self._gcodeList = gcodeList
610                 self._gcodePos = 0
611                 self._printStartTime100 = None
612                 self._printSection = 'CUSTOM'
613                 self._changeState(self.STATE_PRINTING)
614                 self._printStartTime = time.time()
615                 for i in xrange(0, 4):
616                         self._sendNext()
617         
618         def cancelPrint(self):
619                 if self.isOperational():
620                         self._changeState(self.STATE_OPERATIONAL)
621         
622         def setPause(self, pause):
623                 if not pause and self.isPaused():
624                         self._changeState(self.STATE_PRINTING)
625                         for i in xrange(0, 6):
626                                 self._sendNext()
627                 if pause and self.isPrinting():
628                         self._changeState(self.STATE_PAUSED)
629         
630         def setFeedrateModifier(self, type, value):
631                 self._feedRateModifier[type] = value
632
633 def getExceptionString():
634         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
635         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])