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.
5 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
16 from Cura.util import profile
17 from Cura.util import machineCom
18 from Cura.util.printerConnection import printerConnectionBase
20 class serialConnectionGroup(printerConnectionBase.printerConnectionGroup):
22 The serial connection group. Keeps track of all available serial ports,
23 and builds a serialConnection for each port.
26 super(serialConnectionGroup, self).__init__("USB")
27 self._connectionMap = {}
29 def getAvailableConnections(self):
30 if profile.getMachineSetting('serial_port') == 'AUTO':
31 serialList = machineCom.serialList(True)
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()
45 def getPriority(self):
48 class serialConnection(printerConnectionBase.printerConnectionBase):
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.
53 This class communicates with the Cura.serialCommunication module trough stdin/stdout pipes.
55 def __init__(self, port):
56 super(serialConnection, self).__init__(port)
62 self._temperature = []
63 self._targetTemperature = []
64 self._bedTemperature = 0
65 self._targetBedTemperature = 0
68 self._commState = None
69 self._commStateString = None
71 self._printProgress = 0
73 self._pausePosition = None
75 #Load the data into memory for printing, returns True on success
76 def loadGCodeData(self, dataStream):
77 if self.isPrinting() is None:
80 for line in dataStream:
81 #Strip out comments, we do not need to send comments
83 line = line[:line.index(';')]
84 #Strip out whitespace at the beginning/end this saves data to send.
89 self._gcodeData.append(line)
92 #Start printing the previously loaded file
94 if self.isPrinting() or len(self._gcodeData) < 1 or self._process is None:
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
102 self._pausePosition = None
105 cooldown_toolhead = "M104 S0"
107 change_toolhead = "T%d".format(i)
108 self.sendCommand(change_toolhead)
109 self.sendCommand(cooldown_toolhead)
110 self.sendCommand("M140 S0") #Bed
112 def disableSteppers(self):
113 self.sendCommand("M18")
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:
120 self._process.stdin.write('STOP\n')
121 self._printProgress = 0
123 self._pausePosition = None
125 self.disableSteppers()
127 def isPrinting(self):
128 return self._commState == machineCom.MachineCom.STATE_PRINTING
130 #Returns true if we have the ability to pause the file printing.
135 return self._commState == machineCom.MachineCom.STATE_PAUSED
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:
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')
151 if self._printProgress - 5 > start_gcode_lines: # Substract 5 because of the marlin queue
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:
171 if x is not None and y is not None:
172 # Set E relative positioning
173 self.sendCommand("M83")
176 retract = ("E-%f" % retract_amount)
178 #Move the toolhead up
179 newZ = self._ZPosition + moveZ
183 if newZ > self._ZPosition:
184 move = ("Z%f " % (newZ))
185 else: #No z movement, too close to max height
187 retract_and_move = "G1 {} {}F120\n".format(retract, move)
188 self.sendCommand(retract_and_move)
191 self.sendCommand("G1 X%f Y%f F9000\n" % (parkX, parkY))
193 #Disable the E steppers
194 self.sendCommand("M84 E0\n")
195 # Set E absolute positioning
196 self.sendCommand("M82\n")
198 self._pausePosition = (x, y, self._ZPosition, f, e)
199 self._process.stdin.write("PAUSE\n")
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))
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])
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
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)
227 # Return if the printer with this connection type is available
228 def isAvailable(self):
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)
236 #Returns true if we need to establish an active connection. True for serial connections.
237 def hasActiveConnection(self):
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
247 #Close the active connection to the printer
248 def closeActiveConnection(self):
249 if self._process is not None:
250 self._process.terminate()
253 #Is the active connection open right now.
254 def isActiveConnectionOpen(self):
255 if self._process is None:
257 return self._commState == machineCom.MachineCom.STATE_OPERATIONAL or self._commState == machineCom.MachineCom.STATE_PRINTING or self._commState == machineCom.MachineCom.STATE_PAUSED
259 #Are we trying to open an active connection right now.
260 def isActiveConnectionOpening(self):
261 if self._process is None:
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
265 def getTemperature(self, extruder):
266 if extruder >= len(self._temperature):
268 return self._temperature[extruder]
270 def getBedTemperature(self):
271 return self._bedTemperature
273 #Are we able to send a direct command with sendCommand at this moment in time.
274 def isAbleToSendDirectCommand(self):
275 return self.isActiveConnectionOpen()
277 #Directly send a command to the printer.
278 def sendCommand(self, command):
279 if self._process is None:
281 self._process.stdin.write('C:%s\n' % (command))
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
287 #Returns the error log in case there was an error.
288 def getErrorLog(self):
289 return '\n'.join(self._log)
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')]
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()
305 line = line.split(':', 1)
308 elif line[0] == 'log':
309 self._log.append(line[1])
310 if len(self._log) > 30:
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])
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]
326 elif line[0] == 'progress':
327 self._printProgress = int(line[1])
329 elif line[0] == 'changeZ':
330 self._ZPosition = float(line[1])
334 line = self._process.stdout.readline()