chiark / gitweb /
a33528820dd990cfddf6da04193dbe8a16ad1ba9
[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".format(retract, move)
188                                         self.sendCommand(retract_and_move)
189
190                                         #Move the head away
191                                         self.sendCommand("G1 X%f Y%f F9000" % (parkX, parkY))
192
193                                         #Disable the E steppers
194                                         self.sendCommand("M84 E0")
195                                         # Set E absolute positioning
196                                         self.sendCommand("M82")
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
206                                 #Prime the nozzle when changing filament
207                                 self.sendCommand("G1 E%f F120" % (retract_amount)) #Push the filament out
208                                 self.sendCommand("G1 E-%f F120" % (retract_amount)) #retract again
209
210                                 # Position the toolhead to the correct position again
211                                 self.sendCommand("G1 X%f Y%f Z%f F%d" % self._pausePosition[0:4])
212
213                                 # Prime the nozzle again
214                                 self.sendCommand("G1 E%f F120" % (retract_amount))
215                                 # Set proper feedrate
216                                 self.sendCommand("G1 F%d" % (self._pausePosition[3]))
217                                 # Set E absolute position to cancel out any extrude/retract that occured
218                                 self.sendCommand("G92 E%f" % (self._pausePosition[4]))
219                                 # Set E absolute positioning
220                                 self.sendCommand("M82")
221                         self._process.stdin.write("RESUME\n")
222                         self._pausePosition = None
223
224         #Amount of progression of the current print file. 0.0 to 1.0
225         def getPrintProgress(self):
226                 return (self._printProgress, len(self._gcodeData), self._ZPosition)
227
228         # Return if the printer with this connection type is available
229         def isAvailable(self):
230                 return True
231
232         # Get the connection status string. This is displayed to the user and can be used to communicate
233         #  various information to the user.
234         def getStatusString(self):
235                 return "%s" % (self._commStateString)
236
237         #Returns true if we need to establish an active connection. True for serial connections.
238         def hasActiveConnection(self):
239                 return True
240
241         #Open the active connection to the printer so we can send commands
242         def openActiveConnection(self):
243                 self.closeActiveConnection()
244                 self._thread = threading.Thread(target=self._serialCommunicationThread)
245                 self._thread.daemon = True
246                 self._thread.start()
247
248         #Close the active connection to the printer
249         def closeActiveConnection(self):
250                 if self._process is not None:
251                         self._process.terminate()
252                         self._thread.join()
253
254         #Is the active connection open right now.
255         def isActiveConnectionOpen(self):
256                 if self._process is None:
257                         return False
258                 return self._commState == machineCom.MachineCom.STATE_OPERATIONAL or self._commState == machineCom.MachineCom.STATE_PRINTING or self._commState == machineCom.MachineCom.STATE_PAUSED
259
260         #Are we trying to open an active connection right now.
261         def isActiveConnectionOpening(self):
262                 if self._process is None:
263                         return False
264                 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
265
266         def getTemperature(self, extruder):
267                 if extruder >= len(self._temperature):
268                         return None
269                 return self._temperature[extruder]
270
271         def getBedTemperature(self):
272                 return self._bedTemperature
273
274         #Are we able to send a direct command with sendCommand at this moment in time.
275         def isAbleToSendDirectCommand(self):
276                 return self.isActiveConnectionOpen()
277
278         #Directly send a command to the printer.
279         def sendCommand(self, command):
280                 if self._process is None:
281                         return
282                 self._process.stdin.write('C:%s\n' % (command))
283
284         #Returns true if we got some kind of error. The getErrorLog returns all the information to diagnose the problem.
285         def isInErrorState(self):
286                 return self._commState == machineCom.MachineCom.STATE_ERROR or self._commState == machineCom.MachineCom.STATE_CLOSED_WITH_ERROR
287
288         #Returns the error log in case there was an error.
289         def getErrorLog(self):
290                 return '\n'.join(self._log)
291
292         def _serialCommunicationThread(self):
293                 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
294                         cmdList = [os.path.join(os.path.dirname(sys.executable), 'Cura'), '--serialCommunication']
295                         cmdList += [self._portName + ':' + profile.getMachineSetting('serial_baud')]
296                 else:
297                         cmdList = [sys.executable, '-m', 'Cura.serialCommunication']
298                         cmdList += [self._portName, profile.getMachineSetting('serial_baud')]
299                 if platform.system() == "Darwin":
300                         if platform.machine() == 'i386':
301                                 cmdList = ['arch', '-i386'] + cmdList
302                 self._process = subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
303                 line = self._process.stdout.readline()
304                 while len(line) > 0:
305                         line = line.strip()
306                         line = line.split(':', 1)
307                         if line[0] == '':
308                                 pass
309                         elif line[0] == 'log':
310                                 self._log.append(line[1])
311                                 if len(self._log) > 30:
312                                         self._log.pop(0)
313                         elif line[0] == 'temp':
314                                 line = line[1].split(':')
315                                 self._temperature = json.loads(line[0])
316                                 self._targetTemperature = json.loads(line[1])
317                                 self._bedTemperature = float(line[2])
318                                 self._targetBedTemperature = float(line[3])
319                                 self._doCallback()
320                         elif line[0] == 'message':
321                                 self._doCallback(line[1])
322                         elif line[0] == 'state':
323                                 line = line[1].split(':', 1)
324                                 self._commState = int(line[0])
325                                 self._commStateString = line[1]
326                                 self._doCallback('')
327                         elif line[0] == 'progress':
328                                 self._printProgress = int(line[1])
329                                 self._doCallback()
330                         elif line[0] == 'changeZ':
331                                 self._ZPosition = float(line[1])
332                                 self._doCallback()
333                         else:
334                                 print line
335                         line = self._process.stdout.readline()
336                 self._process = None