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