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