chiark / gitweb /
Merge branch 'master' into SteamEngine
[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.*")
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._temp = 0
173                 self._bedTemp = 0
174                 self._targetTemp = 0
175                 self._bedTargetTemp = 0
176                 self._gcodeList = None
177                 self._gcodePos = 0
178                 self._commandQueue = queue.Queue()
179                 self._logQueue = queue.Queue(256)
180                 self._feedRateModifier = {}
181                 self._currentZ = -1
182                 self._heatupWaitStartTime = 0
183                 self._heatupWaitTimeLost = 0.0
184                 self._printStartTime100 = None
185                 
186                 self.thread = threading.Thread(target=self._monitor)
187                 self.thread.daemon = True
188                 self.thread.start()
189         
190         def _changeState(self, newState):
191                 if self._state == newState:
192                         return
193                 oldState = self.getStateString()
194                 self._state = newState
195                 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
196                 self._callback.mcStateChange(newState)
197         
198         def getState(self):
199                 return self._state
200         
201         def getStateString(self):
202                 if self._state == self.STATE_NONE:
203                         return "Offline"
204                 if self._state == self.STATE_OPEN_SERIAL:
205                         return "Opening serial port"
206                 if self._state == self.STATE_DETECT_SERIAL:
207                         return "Detecting serial port"
208                 if self._state == self.STATE_DETECT_BAUDRATE:
209                         return "Detecting baudrate"
210                 if self._state == self.STATE_CONNECTING:
211                         return "Connecting"
212                 if self._state == self.STATE_OPERATIONAL:
213                         return "Operational"
214                 if self._state == self.STATE_PRINTING:
215                         return "Printing"
216                 if self._state == self.STATE_PAUSED:
217                         return "Paused"
218                 if self._state == self.STATE_CLOSED:
219                         return "Closed"
220                 if self._state == self.STATE_ERROR:
221                         return "Error: %s" % (self.getShortErrorString())
222                 if self._state == self.STATE_CLOSED_WITH_ERROR:
223                         return "Error: %s" % (self.getShortErrorString())
224                 return "?%d?" % (self._state)
225         
226         def getShortErrorString(self):
227                 if len(self._errorValue) < 20:
228                         return self._errorValue
229                 return self._errorValue[:20] + "..."
230
231         def getErrorString(self):
232                 return self._errorValue
233         
234         def isClosedOrError(self):
235                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
236
237         def isError(self):
238                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
239         
240         def isOperational(self):
241                 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
242         
243         def isPrinting(self):
244                 return self._state == self.STATE_PRINTING
245         
246         def isPaused(self):
247                 return self._state == self.STATE_PAUSED
248
249         def getPrintPos(self):
250                 return self._gcodePos
251         
252         def getPrintTime(self):
253                 return time.time() - self._printStartTime
254
255         def getPrintTimeRemainingEstimate(self):
256                 if self._printStartTime100 == None or self.getPrintPos() < 200:
257                         return None
258                 printTime = (time.time() - self._printStartTime100) / 60
259                 printTimeTotal = printTime * (len(self._gcodeList) - 100) / (self.getPrintPos() - 100)
260                 printTimeLeft = printTimeTotal - printTime
261                 return printTimeLeft
262         
263         def getTemp(self):
264                 return self._temp
265         
266         def getBedTemp(self):
267                 return self._bedTemp
268         
269         def getLog(self):
270                 ret = []
271                 while not self._logQueue.empty():
272                         ret.append(self._logQueue.get())
273                 for line in ret:
274                         self._logQueue.put(line, False)
275                 return ret
276         
277         def _monitor(self):
278                 #Open the serial port.
279                 if self._port == 'AUTO':
280                         self._changeState(self.STATE_DETECT_SERIAL)
281                         programmer = stk500v2.Stk500v2()
282                         self._log("Serial port list: %s" % (str(serialList(True))))
283                         for p in serialList(True):
284                                 try:
285                                         self._log("Connecting to: %s" % (p))
286                                         programmer.connect(p)
287                                         self._serial = programmer.leaveISP()
288                                         profile.putPreference('serial_port_auto', p)
289                                         break
290                                 except ispBase.IspError as (e):
291                                         self._log("Error while connecting to %s: %s" % (p, str(e)))
292                                         pass
293                                 except:
294                                         self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
295                                 programmer.close()
296                 elif self._port == 'VIRTUAL':
297                         self._changeState(self.STATE_OPEN_SERIAL)
298                         self._serial = VirtualPrinter()
299                 else:
300                         self._changeState(self.STATE_OPEN_SERIAL)
301                         try:
302                                 self._log("Connecting to: %s" % (self._port))
303                                 if self._baudrate == 0:
304                                         self._serial = serial.Serial(str(self._port), 115200, timeout=0.1, writeTimeout=10000)
305                                 else:
306                                         self._serial = serial.Serial(str(self._port), self._baudrate, timeout=2, writeTimeout=10000)
307                         except:
308                                 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
309                 if self._serial == None:
310                         self._log("Failed to open serial port (%s)" % (self._port))
311                         self._errorValue = 'Failed to autodetect serial port.'
312                         self._changeState(self.STATE_ERROR)
313                         return
314                 self._log("Connected to: %s, starting monitor" % (self._serial))
315                 if self._baudrate == 0:
316                         self._changeState(self.STATE_DETECT_BAUDRATE)
317                 else:
318                         self._changeState(self.STATE_CONNECTING)
319
320                 #Start monitoring the serial port.
321                 if self._state == self.STATE_CONNECTING:
322                         timeout = time.time() + 15
323                 else:
324                         timeout = time.time() + 5
325                 tempRequestTimeout = timeout
326                 while True:
327                         line = self._readline()
328                         if line == None:
329                                 break
330                         
331                         #No matter the state, if we see an error, goto the error state and store the error for reference.
332                         if line.startswith('Error:'):
333                                 #Oh YEAH, consistency.
334                                 # Marlin reports an MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
335                                 #       But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
336                                 #       So we can have an extra newline in the most common case. Awesome work people.
337                                 if re.match('Error:[0-9]\n', line):
338                                         line = line.rstrip() + self._readline()
339                                 #Skip the communication errors, as those get corrected.
340                                 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:
341                                         pass
342                                 elif not self.isError():
343                                         self._errorValue = line[6:]
344                                         self._changeState(self.STATE_ERROR)
345                         if ' T:' in line or line.startswith('T:'):
346                                 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
347                                 if ' B:' in line:
348                                         self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
349                                 self._callback.mcTempUpdate(self._temp, self._bedTemp, self._targetTemp, self._bedTargetTemp)
350                                 #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.
351                                 if not 'ok' in line and self._heatupWaitStartTime != 0:
352                                         t = time.time()
353                                         self._heatupWaitTimeLost = t - self._heatupWaitStartTime
354                                         self._heatupWaitStartTime = t
355                         elif line.strip() != '' and line.strip() != 'ok' and not line.startswith('Resend:') and line != 'echo:Unknown command:""\n' and self.isOperational():
356                                 self._callback.mcMessage(line)
357
358                         if self._state == self.STATE_DETECT_BAUDRATE:
359                                 if line == '' or time.time() > timeout:
360                                         if len(self._baudrateDetectList) < 1:
361                                                 self.close()
362                                                 self._errorValue = "No more baudrates to test, and no suitable baudrate found."
363                                                 self._changeState(self.STATE_ERROR)
364                                         elif self._baudrateDetectRetry > 0:
365                                                 self._baudrateDetectRetry -= 1
366                                                 self._serial.write('\n')
367                                                 self._log("Baudrate test retry: %d" % (self._baudrateDetectRetry))
368                                                 self._sendCommand("M105")
369                                                 self._testingBaudrate = True
370                                         else:
371                                                 baudrate = self._baudrateDetectList.pop(0)
372                                                 try:
373                                                         self._serial.baudrate = baudrate
374                                                         self._serial.timeout = 0.5
375                                                         self._log("Trying baudrate: %d" % (baudrate))
376                                                         self._baudrateDetectRetry = 5
377                                                         self._baudrateDetectTestOk = 0
378                                                         timeout = time.time() + 5
379                                                         self._serial.write('\n')
380                                                         self._sendCommand("M105")
381                                                         self._testingBaudrate = True
382                                                 except:
383                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
384                                 elif 'ok' in line and 'T:' in line:
385                                         self._baudrateDetectTestOk += 1
386                                         if self._baudrateDetectTestOk < 10:
387                                                 self._log("Baudrate test ok: %d" % (self._baudrateDetectTestOk))
388                                                 self._sendCommand("M105")
389                                         else:
390                                                 self._sendCommand("M999")
391                                                 self._serial.timeout = 2
392                                                 profile.putPreference('serial_baud_auto', self._serial.baudrate)
393                                                 self._changeState(self.STATE_OPERATIONAL)
394                                 else:
395                                         self._testingBaudrate = False
396                         elif self._state == self.STATE_CONNECTING:
397                                 if line == '':
398                                         self._sendCommand("M105")
399                                 elif 'ok' in line:
400                                         self._changeState(self.STATE_OPERATIONAL)
401                                 if time.time() > timeout:
402                                         self.close()
403                         elif self._state == self.STATE_OPERATIONAL:
404                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
405                                 if line == '':
406                                         self._sendCommand("M105")
407                                         tempRequestTimeout = time.time() + 5
408                         elif self._state == self.STATE_PRINTING:
409                                 if line == '' and time.time() > timeout:
410                                         self._log("Communication timeout during printing, forcing a line")
411                                         line = 'ok'
412                                 #Even when printing request the temperture every 5 seconds.
413                                 if time.time() > tempRequestTimeout:
414                                         self._commandQueue.put("M105")
415                                         tempRequestTimeout = time.time() + 5
416                                 if 'ok' in line:
417                                         timeout = time.time() + 5
418                                         if not self._commandQueue.empty():
419                                                 self._sendCommand(self._commandQueue.get())
420                                         else:
421                                                 self._sendNext()
422                                 elif "resend" in line.lower() or "rs" in line:
423                                         try:
424                                                 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
425                                         except:
426                                                 if "rs" in line:
427                                                         self._gcodePos = int(line.split()[1])
428                 self._log("Connection closed, closing down monitor")
429         
430         def _log(self, message):
431                 self._callback.mcLog(message)
432                 try:
433                         self._logQueue.put(message, False)
434                 except:
435                         #If the log queue is full, remove the first message and append the new message again
436                         self._logQueue.get()
437                         try:
438                                 self._logQueue.put(message, False)
439                         except:
440                                 pass
441
442         def _readline(self):
443                 if self._serial == None:
444                         return None
445                 try:
446                         ret = self._serial.readline()
447                 except:
448                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
449                         self._errorValue = getExceptionString()
450                         self.close(True)
451                         return None
452                 if ret == '':
453                         #self._log("Recv: TIMEOUT")
454                         return ''
455                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
456                 return ret
457         
458         def close(self, isError = False):
459                 if self._serial != None:
460                         self._serial.close()
461                         if isError:
462                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
463                         else:
464                                 self._changeState(self.STATE_CLOSED)
465                 self._serial = None
466         
467         def __del__(self):
468                 self.close()
469         
470         def _sendCommand(self, cmd):
471                 if self._serial is None:
472                         return
473                 if 'M109' in cmd or 'M190' in cmd:
474                         self._heatupWaitStartTime = time.time()
475                 if 'M104' in cmd or 'M109' in cmd:
476                         try:
477                                 self._targetTemp = float(re.search('S([0-9]+)', cmd).group(1))
478                         except:
479                                 pass
480                 if 'M140' in cmd or 'M190' in cmd:
481                         try:
482                                 self._bedTargetTemp = float(re.search('S([0-9]+)', cmd).group(1))
483                         except:
484                                 pass
485                 self._log('Send: %s' % (cmd))
486                 try:
487                         self._serial.write(cmd + '\n')
488                 except serial.SerialTimeoutException:
489                         self._log("Serial timeout while writing to serial port, trying again.")
490                         try:
491                                 self._serial.write(cmd + '\n')
492                         except:
493                                 self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
494                                 self._errorValue = getExceptionString()
495                                 self.close(True)
496                 except:
497                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
498                         self._errorValue = getExceptionString()
499                         self.close(True)
500         
501         def _sendNext(self):
502                 if self._gcodePos >= len(self._gcodeList):
503                         self._changeState(self.STATE_OPERATIONAL)
504                         return
505                 if self._gcodePos == 100:
506                         self._printStartTime100 = time.time()
507                 line = self._gcodeList[self._gcodePos]
508                 if type(line) is tuple:
509                         self._printSection = line[1]
510                         line = line[0]
511                 try:
512                         if line == 'M0' or line == 'M1':
513                                 self.setPause(True)
514                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
515                         if self._printSection in self._feedRateModifier:
516                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
517                         if ('G0' in line or 'G1' in line) and 'Z' in line:
518                                 z = float(re.search('Z([0-9\.]*)', line).group(1))
519                                 if self._currentZ != z:
520                                         self._currentZ = z
521                                         self._callback.mcZChange(z)
522                 except:
523                         self._log("Unexpected error: %s" % (getExceptionString()))
524                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
525                 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
526                 self._gcodePos += 1
527                 self._callback.mcProgress(self._gcodePos)
528         
529         def sendCommand(self, cmd):
530                 cmd = cmd.encode('ascii', 'replace')
531                 if self.isPrinting():
532                         self._commandQueue.put(cmd)
533                 elif self.isOperational():
534                         self._sendCommand(cmd)
535         
536         def printGCode(self, gcodeList):
537                 if not self.isOperational() or self.isPrinting():
538                         return
539                 self._gcodeList = gcodeList
540                 self._gcodePos = 0
541                 self._printStartTime100 = None
542                 self._printSection = 'CUSTOM'
543                 self._changeState(self.STATE_PRINTING)
544                 self._printStartTime = time.time()
545                 for i in xrange(0, 4):
546                         self._sendNext()
547         
548         def cancelPrint(self):
549                 if self.isOperational():
550                         self._changeState(self.STATE_OPERATIONAL)
551         
552         def setPause(self, pause):
553                 if not pause and self.isPaused():
554                         self._changeState(self.STATE_PRINTING)
555                         for i in xrange(0, 6):
556                                 self._sendNext()
557                 if pause and self.isPrinting():
558                         self._changeState(self.STATE_PAUSED)
559         
560         def setFeedrateModifier(self, type, value):
561                 self._feedRateModifier[type] = value
562
563 def getExceptionString():
564         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
565         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])