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