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