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