chiark / gitweb /
Merge branch 'master' of github.com:daid/Cura
[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 from serial 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(data[data.find('S')+1:])
66                         except:
67                                 pass
68                 if 'M140' in data or 'M190' in data:
69                         try:
70                                 self.bedTargetTemp = float(data[data.find('S')+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                 print(message)
105         
106         def mcTempUpdate(self, temp, bedTemp):
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._gcodeList = None
155                 self._gcodePos = 0
156                 self._commandQueue = queue.Queue()
157                 self._logQueue = queue.Queue(256)
158                 self._feedRateModifier = {}
159                 self._currentZ = -1
160                 self._heatupWaitStartTime = 0
161                 self._heatupWaitTimeLost = 0.0
162                 
163                 self.thread = threading.Thread(target=self._monitor)
164                 self.thread.daemon = True
165                 self.thread.start()
166         
167         def _changeState(self, newState):
168                 if self._state == newState:
169                         return
170                 oldState = self.getStateString()
171                 self._state = newState
172                 self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
173                 self._callback.mcStateChange(newState)
174         
175         def getState(self):
176                 return self._state
177         
178         def getStateString(self):
179                 if self._state == self.STATE_NONE:
180                         return "Offline"
181                 if self._state == self.STATE_OPEN_SERIAL:
182                         return "Opening serial port"
183                 if self._state == self.STATE_DETECT_SERIAL:
184                         return "Detecting serial port"
185                 if self._state == self.STATE_DETECT_BAUDRATE:
186                         return "Detecting baudrate"
187                 if self._state == self.STATE_CONNECTING:
188                         return "Connecting"
189                 if self._state == self.STATE_OPERATIONAL:
190                         return "Operational"
191                 if self._state == self.STATE_PRINTING:
192                         return "Printing"
193                 if self._state == self.STATE_PAUSED:
194                         return "Paused"
195                 if self._state == self.STATE_CLOSED:
196                         return "Closed"
197                 if self._state == self.STATE_ERROR:
198                         return "Error: %s" % (self._errorValue)
199                 if self._state == self.STATE_CLOSED_WITH_ERROR:
200                         return "Error: %s" % (self._errorValue)
201                 return "?%d?" % (self._state)
202         
203         def isClosedOrError(self):
204                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
205
206         def isError(self):
207                 return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
208         
209         def isOperational(self):
210                 return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
211         
212         def isPrinting(self):
213                 return self._state == self.STATE_PRINTING
214         
215         def getPrintPos(self):
216                 return self._gcodePos
217         
218         def getPrintTime(self):
219                 return time.time() - self._printStartTime - self._heatupWaitTimeLost
220         
221         def isPaused(self):
222                 return self._state == self.STATE_PAUSED
223         
224         def getTemp(self):
225                 return self._temp
226         
227         def getBedTemp(self):
228                 return self._bedTemp
229         
230         def getLog(self):
231                 ret = []
232                 while not self._logQueue.empty():
233                         ret.append(self._logQueue.get())
234                 for line in ret:
235                         self._logQueue.put(line, False)
236                 return ret
237         
238         def _monitor(self):
239                 #Open the serial port.
240                 if self._port == 'AUTO':
241                         self._changeState(self.STATE_DETECT_SERIAL)
242                         programmer = stk500v2.Stk500v2()
243                         self._log("Serial port list: %s" % (str(serialList())))
244                         for p in serialList():
245                                 try:
246                                         self._log("Connecting to: %s" % (p))
247                                         programmer.connect(p)
248                                         self._serial = programmer.leaveISP()
249                                         profile.putPreference('serial_port_auto', p)
250                                         break
251                                 except ispBase.IspError as (e):
252                                         self._log("Error while connecting to %s: %s" % (p, str(e)))
253                                         pass
254                                 except:
255                                         self._log("Unexpected error while connecting to serial port: %s %s" % (p, getExceptionString()))
256                                 programmer.close()
257                 elif self._port == 'VIRTUAL':
258                         self._changeState(self.STATE_OPEN_SERIAL)
259                         self._serial = VirtualPrinter()
260                 else:
261                         self._changeState(self.STATE_OPEN_SERIAL)
262                         try:
263                                 self._log("Connecting to: %s" % (self._port))
264                                 if self._baudrate == 0:
265                                         self._serial = Serial(self._port, 115200, timeout=0.1, writeTimeout=10000)
266                                 else:
267                                         self._serial = Serial(self._port, self._baudrate, timeout=2, writeTimeout=10000)
268                         except:
269                                 self._log("Unexpected error while connecting to serial port: %s %s" % (self._port, getExceptionString()))
270                 if self._serial == None:
271                         self._log("Failed to open serial port (%s)" % (self._port))
272                         self._errorValue = 'Failed to autodetect serial port.'
273                         self._changeState(self.STATE_ERROR)
274                         return
275                 self._log("Connected to: %s, starting monitor" % (self._serial))
276                 if self._baudrate == 0:
277                         self._changeState(self.STATE_DETECT_BAUDRATE)
278                 else:
279                         self._changeState(self.STATE_CONNECTING)
280
281                 #Start monitoring the serial port.
282                 timeout = time.time() + 5
283                 tempRequestTimeout = timeout
284                 while True:
285                         line = self._readline()
286                         if line == None:
287                                 break
288                         
289                         #No matter the state, if we see an error, goto the error state and store the error for reference.
290                         if line.startswith('Error: '):
291                                 #Oh YEAH, consistency.
292                                 # Marlin reports an MIN/MAX temp error as "Error: x\n: Extruder switched off. MAXTEMP triggered !\n"
293                                 #       But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
294                                 #       So we can have an extra newline in the most common case. Awesome work people.
295                                 if re.match('Error: [0-9]\n', line):
296                                         line = line.rstrip() + self._readline()
297                                 self._errorValue = line
298                                 self._changeState(self.STATE_ERROR)
299                         if ' T:' in line or line.startswith('T:'):
300                                 self._temp = float(re.search("[0-9\.]*", line.split('T:')[1]).group(0))
301                                 if ' B:' in line:
302                                         self._bedTemp = float(re.search("[0-9\.]*", line.split(' B:')[1]).group(0))
303                                 self._callback.mcTempUpdate(self._temp, self._bedTemp)
304                                 #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.
305                                 if not 'ok' in line and self._heatupWaitStartTime != 0:
306                                         t = time.time()
307                                         self._heatupWaitTimeLost = t - self._heatupWaitStartTime
308                                         self._heatupWaitStartTime = t
309                         elif line.strip() != 'ok' and self.isOperational():
310                                 self._callback.mcMessage(line)
311
312                         if self._state == self.STATE_DETECT_BAUDRATE:
313                                 if line == '' or time.time() > timeout:
314                                         if len(self._baudrateDetectList) < 1:
315                                                 self._log("No more baudrates to test, and no suitable baudrate found.")
316                                                 self.close()
317                                         elif self._baudrateDetectRetry > 0:
318                                                 self._baudrateDetectRetry -= 1
319                                                 self._serial.write('\n')
320                                                 self._sendCommand("M105")
321                                         else:
322                                                 baudrate = self._baudrateDetectList.pop(0)
323                                                 try:
324                                                         self._serial.baudrate = baudrate
325                                                         self._serial.timeout = 0.5
326                                                         self._log("Trying baudrate: %d" % (baudrate))
327                                                         self._baudrateDetectRetry = 5
328                                                         timeout = time.time() + 5
329                                                         self._serial.write('\n')
330                                                         self._sendCommand("M105")
331                                                 except:
332                                                         self._log("Unexpected error while setting baudrate: %d %s" % (baudrate, getExceptionString()))
333                                 elif 'ok' in line:
334                                         self._serial.timeout = 2
335                                         profile.putPreference('serial_baud_auto', self._serial.baudrate)
336                                         self._changeState(self.STATE_OPERATIONAL)
337                         elif self._state == self.STATE_CONNECTING:
338                                 if line == '':
339                                         self._sendCommand("M105")
340                                 elif 'ok' in line:
341                                         self._changeState(self.STATE_OPERATIONAL)
342                                 if time.time() > timeout:
343                                         self.close()
344                         elif self._state == self.STATE_OPERATIONAL:
345                                 #Request the temperature on comm timeout (every 2 seconds) when we are not printing.
346                                 if line == '':
347                                         self._sendCommand("M105")
348                                         tempRequestTimeout = time.time() + 5
349                         elif self._state == self.STATE_PRINTING:
350                                 if line == '' and time.time() > timeout:
351                                         self._log("Communication timeout during printing, forcing a line")
352                                         line = 'ok'
353                                 #Even when printing request the temperture every 5 seconds.
354                                 if time.time() > tempRequestTimeout:
355                                         self._commandQueue.put("M105")
356                                         tempRequestTimeout = time.time() + 5
357                                 if 'ok' in line:
358                                         timeout = time.time() + 5
359                                         if not self._commandQueue.empty():
360                                                 self._sendCommand(self._commandQueue.get())
361                                         else:
362                                                 self._sendNext()
363                                 elif "resend" in line.lower() or "rs" in line:
364                                         try:
365                                                 self._gcodePos = int(line.replace("N:"," ").replace("N"," ").replace(":"," ").split()[-1])
366                                         except:
367                                                 if "rs" in line:
368                                                         self._gcodePos = int(line.split()[1])
369                 self._log("Connection closed, closing down monitor")
370         
371         def _log(self, message):
372                 self._callback.mcLog(message)
373                 try:
374                         self._logQueue.put(message, False)
375                 except:
376                         #If the log queue is full, remove the first message and append the new message again
377                         self._logQueue.get()
378                         self._logQueue.put(message, False)
379
380         def _readline(self):
381                 if self._serial == None:
382                         return None
383                 try:
384                         ret = self._serial.readline()
385                 except:
386                         self._log("Unexpected error while reading serial port: %s" % (getExceptionString()))
387                         self._errorValue = getExceptionString()
388                         self.close(True)
389                         return None
390                 if ret == '':
391                         #self._log("Recv: TIMEOUT")
392                         return ''
393                 self._log("Recv: %s" % (unicode(ret, 'ascii', 'replace').encode('ascii', 'replace').rstrip()))
394                 return ret
395         
396         def close(self, isError = False):
397                 if self._serial != None:
398                         self._serial.close()
399                         if isError:
400                                 self._changeState(self.STATE_CLOSED_WITH_ERROR)
401                         else:
402                                 self._changeState(self.STATE_CLOSED)
403                 self._serial = None
404         
405         def __del__(self):
406                 self.close()
407         
408         def _sendCommand(self, cmd):
409                 if self._serial == None:
410                         return
411                 if 'M109' in cmd or 'M190' in cmd:
412                         self._heatupWaitStartTime = time.time()
413                 self._log('Send: %s' % (cmd))
414                 try:
415                         #TODO: This can throw a write timeout exception, but we do not want timeout on writes. Find a fix for this.
416                         #       Oddly enough, the write timeout is not even set and thus we should not get a write timeout.
417                         self._serial.write(cmd + '\n')
418                 except:
419                         self._log("Unexpected error while writing serial port: %s" % (getExceptionString()))
420                         self._errorValue = getExceptionString()
421                         self.close(True)
422         
423         def _sendNext(self):
424                 if self._gcodePos >= len(self._gcodeList):
425                         self._changeState(self.STATE_OPERATIONAL)
426                         return
427                 line = self._gcodeList[self._gcodePos]
428                 if type(line) is tuple:
429                         self._printSection = line[1]
430                         line = line[0]
431                 try:
432                         if line == 'M0' or line == 'M1':
433                                 self.setPause(True)
434                                 line = 'M105'   #Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
435                         if self._printSection in self._feedRateModifier:
436                                 line = re.sub('F([0-9]*)', lambda m: 'F' + str(int(int(m.group(1)) * self._feedRateModifier[self._printSection])), line)
437                         if ('G0' in line or 'G1' in line) and 'Z' in line:
438                                 z = float(re.search('Z([0-9\.]*)', line).group(1))
439                                 if self._currentZ != z:
440                                         self._currentZ = z
441                                         self._callback.mcZChange(z)
442                 except:
443                         self._log("Unexpected error: %s" % (getExceptionString()))
444                 checksum = reduce(lambda x,y:x^y, map(ord, "N%d%s" % (self._gcodePos, line)))
445                 self._sendCommand("N%d%s*%d" % (self._gcodePos, line, checksum))
446                 self._gcodePos += 1
447                 self._callback.mcProgress(self._gcodePos)
448         
449         def sendCommand(self, cmd):
450                 cmd = cmd.encode('ascii', 'replace')
451                 if self.isPrinting():
452                         self._commandQueue.put(cmd)
453                 elif self.isOperational():
454                         self._sendCommand(cmd)
455         
456         def printGCode(self, gcodeList):
457                 if not self.isOperational() or self.isPrinting():
458                         return
459                 self._gcodeList = gcodeList
460                 self._gcodePos = 0
461                 self._printSection = 'CUSTOM'
462                 self._changeState(self.STATE_PRINTING)
463                 self._printStartTime = time.time()
464                 for i in xrange(0, 6):
465                         self._sendNext()
466         
467         def cancelPrint(self):
468                 if self.isOperational():
469                         self._changeState(self.STATE_OPERATIONAL)
470         
471         def setPause(self, pause):
472                 if not pause and self.isPaused():
473                         self._changeState(self.STATE_PRINTING)
474                         for i in xrange(0, 6):
475                                 self._sendNext()
476                 if pause and self.isPrinting():
477                         self._changeState(self.STATE_PAUSED)
478         
479         def setFeedrateModifier(self, type, value):
480                 self._feedRateModifier[type] = value
481
482 def getExceptionString():
483         locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
484         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])
485