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