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