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