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