chiark / gitweb /
bf37ea64414d4542ce36e4306870fa32b511cfa7
[cura.git] / Cura / util / printerConnection / serialConnection.py
1 """
2 The serial/USB printer connection. Uses a 2nd python process to connect to the printer so we never
3 have locking problems where other threads in python can block the USB printing.
4 """
5 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
6
7 import threading
8 import time
9 import platform
10 import os
11 import sys
12 import subprocess
13 import json
14 import re
15
16 from Cura.util import profile
17 from Cura.util import machineCom
18 from Cura.util.printerConnection import printerConnectionBase
19
20 class serialConnectionGroup(printerConnectionBase.printerConnectionGroup):
21         """
22         The serial connection group. Keeps track of all available serial ports,
23         and builds a serialConnection for each port.
24         """
25         def __init__(self):
26                 super(serialConnectionGroup, self).__init__("USB")
27                 self._connectionMap = {}
28
29         def getAvailableConnections(self):
30                 if profile.getMachineSetting('serial_port') == 'AUTO':
31                         serialList = machineCom.serialList(True)
32                 else:
33                         serialList = [profile.getMachineSetting('serial_port')]
34                 for port in serialList:
35                         if port not in self._connectionMap:
36                                 self._connectionMap[port] = serialConnection(port)
37                 for key in self._connectionMap.keys():
38                         if key not in serialList and not self._connectionMap[key].isActiveConnectionOpen():
39                                 self._connectionMap.pop(key)
40                 return self._connectionMap.values()
41
42         def getIconID(self):
43                 return 6
44
45         def getPriority(self):
46                 return 50
47
48 class serialConnection(printerConnectionBase.printerConnectionBase):
49         """
50         A serial connection. Needs to build an active-connection.
51         When an active connection is created, a 2nd python process is spawned which handles the actual serial communication.
52
53         This class communicates with the Cura.serialCommunication module trough stdin/stdout pipes.
54         """
55         def __init__(self, port):
56                 super(serialConnection, self).__init__(port)
57                 self._portName = port
58
59                 self._process = None
60                 self._thread = None
61
62                 self._temperature = []
63                 self._targetTemperature = []
64                 self._bedTemperature = 0
65                 self._targetBedTemperature = 0
66                 self._log = []
67
68                 self._commState = None
69                 self._commStateString = None
70                 self._gcodeData = []
71                 self._printProgress = 0
72                 self._ZPosition = 0
73                 self._pausePosition = None
74
75         #Load the data into memory for printing, returns True on success
76         def loadGCodeData(self, dataStream):
77                 if self.isPrinting() is None:
78                         return False
79                 self._gcodeData = []
80                 for line in dataStream:
81                         #Strip out comments, we do not need to send comments
82                         if ';' in line:
83                                 line = line[:line.index(';')]
84                         #Strip out whitespace at the beginning/end this saves data to send.
85                         line = line.strip()
86
87                         if len(line) < 1:
88                                 continue
89                         self._gcodeData.append(line)
90                 return True
91
92         #Start printing the previously loaded file
93         def startPrint(self):
94                 if self.isPrinting() or len(self._gcodeData) < 1 or self._process is None:
95                         return
96                 self._process.stdin.write('STOP\n')
97                 for line in self._gcodeData:
98                         self._process.stdin.write('G:%s\n' % (line))
99                 self._process.stdin.write('START\n')
100                 self._printProgress = 0
101                 self._ZPosition = 0
102                 self._pausePosition = None
103
104         def coolDown(self):
105                 cooldown_toolhead = "M104 S0"
106                 for i in range(0,3):
107                         change_toolhead = "T%d".format(i)
108                         self.sendCommand(change_toolhead)
109                         self.sendCommand(cooldown_toolhead)
110                 self.sendCommand("M140 S0") #Bed
111
112         def disableSteppers(self):
113                 self.sendCommand("M18")
114
115         #Abort the previously loaded print file
116         def cancelPrint(self):
117                 if (not self.isPrinting() and not self.isPaused()) or \
118                         self._process is None:
119                         return
120                 self._process.stdin.write('STOP\n')
121                 self._printProgress = 0
122                 self._ZPosition = 0
123                 self._pausePosition = None
124                 self.coolDown()
125                 self.disableSteppers()
126
127         def isPrinting(self):
128                 return self._commState == machineCom.MachineCom.STATE_PRINTING
129
130         #Returns true if we have the ability to pause the file printing.
131         def hasPause(self):
132                 return True
133
134         def isPaused(self):
135                 return self._commState == machineCom.MachineCom.STATE_PAUSED
136
137         #Pause or unpause the printing depending on the value, if supported.
138         def pause(self, value):
139                 if not (self.isPrinting() or self.isPaused()) or self._process is None:
140                         return
141                 if value:
142                         start_gcode = profile.getAlterationFileContents('start.gcode')
143                         start_gcode_lines = len(start_gcode.split("\n"))
144                         parkX = profile.getMachineSettingFloat('machine_width') - 10
145                         parkY = profile.getMachineSettingFloat('machine_depth') - 10
146                         maxZ = profile.getMachineSettingFloat('machine_height') - 10
147                         #retract_amount = profile.getProfileSettingFloat('retraction_amount')
148                         retract_amount = 5.0
149                         moveZ = 10.0
150
151                         if self._printProgress - 5 > start_gcode_lines: # Substract 5 because of the marlin queue
152                                 x = None
153                                 y = None
154                                 e = None
155                                 f = None
156                                 for i in xrange(self._printProgress - 1, start_gcode_lines, -1):
157                                         line = self._gcodeData[i]
158                                         if ('G0' in line or 'G1' in line) and 'X' in line and x is None:
159                                                 x = float(re.search('X(-?[0-9\.]*)', line).group(1))
160                                         if ('G0' in line or 'G1' in line) and 'Y' in line and y is None:
161                                                 y = float(re.search('Y(-?[0-9\.]*)', line).group(1))
162                                         if ('G0' in line or 'G1' in line) and 'E' in line and e is None:
163                                                 e = float(re.search('E(-?[0-9\.]*)', line).group(1))
164                                         if ('G0' in line or 'G1' in line) and 'F' in line and f is None:
165                                                 f = int(re.search('F(-?[0-9\.]*)', line).group(1))
166                                         if x is not None and y is not None and f is not None and e is not None:
167                                                 break
168                                 if f is None:
169                                         f = 1200
170
171                                 if x is not None and y is not None:
172                                         # Set E relative positioning
173                                         self.sendCommand("M83")
174                                         
175                                         # Retract 1mm
176                                         retract = ("E-%f" % retract_amount)
177
178                                         #Move the toolhead up
179                                         newZ = self._ZPosition + moveZ
180                                         if maxZ < newZ:
181                                                 newZ = maxZ
182                                                 
183                                         if newZ > self._ZPosition:
184                                                 move = ("Z%f " % (newZ))
185                                         else: #No z movement, too close to max height 
186                                                 move = ""
187                                         retract_and_move = "G1 {} {}F120\n".format(retract, move)
188                                         self.sendCommand(retract_and_move)
189
190                                         #Move the head away
191                                         self.sendCommand("G1 X%f Y%f F9000\n" % (parkX, parkY))
192
193                                         #Disable the E steppers
194                                         self.sendCommand("M84 E0\n")
195                                         # Set E absolute positioning
196                                         self.sendCommand("M82\n")
197
198                                         self._pausePosition = (x, y, self._ZPosition, f, e)
199                         self._process.stdin.write("PAUSE\n")
200                 else:
201                         if self._pausePosition:
202                                 retract_amount = profile.getProfileSettingFloat('retraction_amount')
203                                 # Set E relative positioning
204                                 self.sendCommand("M83")
205                                 #Push the filament back, and retract again, the properly primes the nozzle when changing filament.
206                                 self.sendCommand("G1 E%f F120\n" % (retract_amount))
207                                 self.sendCommand("G1 E-%f F120\n" % (retract_amount))
208
209                                 # Position the toolhead to the correct position again
210                                 self.sendCommand("G1 X%f Y%f Z%f F%d\n" % self._pausePosition[0:4])
211
212                                 # Prime the nozzle again
213                                 self.sendCommand("G1 E%f F120\n" % (retract_amount))
214                                 # Set proper feedrate
215                                 self.sendCommand("G1 F%d\n" % (self._pausePosition[3]))
216                                 # Set E absolute position to cancel out any extrude/retract that occured
217                                 self.sendCommand("G92 E%f\n" % (self._pausePosition[4]))
218                                 # Set E absolute positioning
219                                 self.sendCommand("M82\n")
220                         self._process.stdin.write("RESUME\n")
221                         self._pausePosition = None
222
223         #Amount of progression of the current print file. 0.0 to 1.0
224         def getPrintProgress(self):
225                 return (self._printProgress, len(self._gcodeData), self._ZPosition)
226
227         # Return if the printer with this connection type is available
228         def isAvailable(self):
229                 return True
230
231         # Get the connection status string. This is displayed to the user and can be used to communicate
232         #  various information to the user.
233         def getStatusString(self):
234                 return "%s" % (self._commStateString)
235
236         #Returns true if we need to establish an active connection. True for serial connections.
237         def hasActiveConnection(self):
238                 return True
239
240         #Open the active connection to the printer so we can send commands
241         def openActiveConnection(self):
242                 self.closeActiveConnection()
243                 self._thread = threading.Thread(target=self._serialCommunicationThread)
244                 self._thread.daemon = True
245                 self._thread.start()
246
247         #Close the active connection to the printer
248         def closeActiveConnection(self):
249                 if self._process is not None:
250                         self._process.terminate()
251                         self._thread.join()
252
253         #Is the active connection open right now.
254         def isActiveConnectionOpen(self):
255                 if self._process is None:
256                         return False
257                 return self._commState == machineCom.MachineCom.STATE_OPERATIONAL or self._commState == machineCom.MachineCom.STATE_PRINTING or self._commState == machineCom.MachineCom.STATE_PAUSED
258
259         #Are we trying to open an active connection right now.
260         def isActiveConnectionOpening(self):
261                 if self._process is None:
262                         return False
263                 return self._commState == machineCom.MachineCom.STATE_OPEN_SERIAL or self._commState == machineCom.MachineCom.STATE_CONNECTING or self._commState == machineCom.MachineCom.STATE_DETECT_SERIAL or self._commState == machineCom.MachineCom.STATE_DETECT_BAUDRATE
264
265         def getTemperature(self, extruder):
266                 if extruder >= len(self._temperature):
267                         return None
268                 return self._temperature[extruder]
269
270         def getBedTemperature(self):
271                 return self._bedTemperature
272
273         #Are we able to send a direct command with sendCommand at this moment in time.
274         def isAbleToSendDirectCommand(self):
275                 return self.isActiveConnectionOpen()
276
277         #Directly send a command to the printer.
278         def sendCommand(self, command):
279                 if self._process is None:
280                         return
281                 self._process.stdin.write('C:%s\n' % (command))
282
283         #Returns true if we got some kind of error. The getErrorLog returns all the information to diagnose the problem.
284         def isInErrorState(self):
285                 return self._commState == machineCom.MachineCom.STATE_ERROR or self._commState == machineCom.MachineCom.STATE_CLOSED_WITH_ERROR
286
287         #Returns the error log in case there was an error.
288         def getErrorLog(self):
289                 return '\n'.join(self._log)
290
291         def _serialCommunicationThread(self):
292                 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
293                         cmdList = [os.path.join(os.path.dirname(sys.executable), 'Cura'), '--serialCommunication']
294                         cmdList += [self._portName + ':' + profile.getMachineSetting('serial_baud')]
295                 else:
296                         cmdList = [sys.executable, '-m', 'Cura.serialCommunication']
297                         cmdList += [self._portName, profile.getMachineSetting('serial_baud')]
298                 if platform.system() == "Darwin":
299                         if platform.machine() == 'i386':
300                                 cmdList = ['arch', '-i386'] + cmdList
301                 self._process = subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
302                 line = self._process.stdout.readline()
303                 while len(line) > 0:
304                         line = line.strip()
305                         line = line.split(':', 1)
306                         if line[0] == '':
307                                 pass
308                         elif line[0] == 'log':
309                                 self._log.append(line[1])
310                                 if len(self._log) > 30:
311                                         self._log.pop(0)
312                         elif line[0] == 'temp':
313                                 line = line[1].split(':')
314                                 self._temperature = json.loads(line[0])
315                                 self._targetTemperature = json.loads(line[1])
316                                 self._bedTemperature = float(line[2])
317                                 self._targetBedTemperature = float(line[3])
318                                 self._doCallback()
319                         elif line[0] == 'message':
320                                 self._doCallback(line[1])
321                         elif line[0] == 'state':
322                                 line = line[1].split(':', 1)
323                                 self._commState = int(line[0])
324                                 self._commStateString = line[1]
325                                 self._doCallback('')
326                         elif line[0] == 'progress':
327                                 self._printProgress = int(line[1])
328                                 self._doCallback()
329                         elif line[0] == 'changeZ':
330                                 self._ZPosition = float(line[1])
331                                 self._doCallback()
332                         else:
333                                 print line
334                         line = self._process.stdout.readline()
335                 self._process = None