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