chiark / gitweb /
8999c80b69ce4b36f310ed6d6f4c4571e3aee161
[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, int(profile.getMachineSetting('extruder_amount'))):
107                         change_toolhead = "T{}".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         def setLCDmessage(self):
116                 cancel_text = "Print canceled"
117                 formatted_command = "M117 {}".format(cancel_text)
118                 self.sendCommand(formatted_command)
119
120         #Abort the previously loaded print file
121         def cancelPrint(self):
122                 if (not self.isPrinting() and not self.isPaused()) or \
123                         self._process is None:
124                         return
125                 self._process.stdin.write('STOP\n')
126                 self._printProgress = 0
127                 self._ZPosition = 0
128                 self._pausePosition = None
129                 self.coolDown()
130                 self.disableSteppers()
131                 self.setLCDmessage()
132
133         def isPrinting(self):
134                 return self._commState == machineCom.MachineCom.STATE_PRINTING
135
136         #Returns true if we have the ability to pause the file printing.
137         def hasPause(self):
138                 return True
139
140         def isPaused(self):
141                 return self._commState == machineCom.MachineCom.STATE_PAUSED
142
143         #Pause or unpause the printing depending on the value, if supported.
144         def pause(self, value):
145                 if not (self.isPrinting() or self.isPaused()) or self._process is None:
146                         return
147                 if value:
148                         start_gcode = profile.getAlterationFileContents('start.gcode')
149                         start_gcode_lines = len(start_gcode.split("\n"))
150                         parkX = profile.getMachineSettingFloat('machine_width') - 10
151                         parkY = profile.getMachineSettingFloat('machine_depth') - 10
152                         maxZ = profile.getMachineSettingFloat('machine_height') - 10
153                         #retract_amount = profile.getProfileSettingFloat('retraction_amount')
154                         retract_amount = 5.0
155                         moveZ = 10.0
156
157                         self._process.stdin.write("PAUSE\n")
158                         if self._printProgress - 5 > start_gcode_lines: # Substract 5 because of the marlin queue
159                                 x = None
160                                 y = None
161                                 e = None
162                                 f = None
163                                 for i in xrange(self._printProgress - 1, start_gcode_lines, -1):
164                                         line = self._gcodeData[i]
165                                         if ('G0' in line or 'G1' in line) and 'X' in line and x is None:
166                                                 x = float(re.search('X(-?[0-9\.]*)', line).group(1))
167                                         if ('G0' in line or 'G1' in line) and 'Y' in line and y is None:
168                                                 y = float(re.search('Y(-?[0-9\.]*)', line).group(1))
169                                         if ('G0' in line or 'G1' in line) and 'E' in line and e is None:
170                                                 e = float(re.search('E(-?[0-9\.]*)', line).group(1))
171                                         if ('G0' in line or 'G1' in line) and 'F' in line and f is None:
172                                                 f = int(re.search('F(-?[0-9\.]*)', line).group(1))
173                                         if x is not None and y is not None and f is not None and e is not None:
174                                                 break
175                                 if f is None:
176                                         f = 1200
177
178                                 if x is not None and y is not None:
179                                         # Set E relative positioning
180                                         self.sendCommand("M83")
181
182                                         # Retract 1mm
183                                         retract = ("E-%f" % retract_amount)
184
185                                         #Move the toolhead up
186                                         newZ = self._ZPosition + moveZ
187                                         if maxZ < newZ:
188                                                 newZ = maxZ
189
190                                         if newZ > self._ZPosition:
191                                                 move = ("Z%f " % (newZ))
192                                         else: #No z movement, too close to max height 
193                                                 move = ""
194                                         retract_and_move = "G1 {} {}F120".format(retract, move)
195                                         self.sendCommand(retract_and_move)
196
197                                         #Move the head away
198                                         self.sendCommand("G1 X%f Y%f F9000" % (parkX, parkY))
199
200                                         #Disable the E steppers
201                                         self.sendCommand("M84 E0")
202                                         # Set E absolute positioning
203                                         self.sendCommand("M82")
204
205                                         self._pausePosition = (x, y, self._ZPosition, f, e)
206                 else:
207                         if self._pausePosition:
208                                 retract_amount = profile.getProfileSettingFloat('retraction_amount')
209                                 # Set E relative positioning
210                                 self.sendCommand("M83")
211
212                                 #Prime the nozzle when changing filament
213                                 self.sendCommand("G1 E%f F120" % (retract_amount)) #Push the filament out
214                                 self.sendCommand("G1 E-%f F120" % (retract_amount)) #retract again
215
216                                 # Position the toolhead to the correct position again
217                                 self.sendCommand("G1 X%f Y%f Z%f F%d" % self._pausePosition[0:4])
218
219                                 # Prime the nozzle again
220                                 self.sendCommand("G1 E%f F120" % (retract_amount))
221                                 # Set proper feedrate
222                                 self.sendCommand("G1 F%d" % (self._pausePosition[3]))
223                                 # Set E absolute position to cancel out any extrude/retract that occured
224                                 self.sendCommand("G92 E%f" % (self._pausePosition[4]))
225                                 # Set E absolute positioning
226                                 self.sendCommand("M82")
227                         self._process.stdin.write("RESUME\n")
228                         self._pausePosition = None
229
230         #Amount of progression of the current print file. 0.0 to 1.0
231         def getPrintProgress(self):
232                 return (self._printProgress, len(self._gcodeData), self._ZPosition)
233
234         # Return if the printer with this connection type is available
235         def isAvailable(self):
236                 return True
237
238         # Get the connection status string. This is displayed to the user and can be used to communicate
239         #  various information to the user.
240         def getStatusString(self):
241                 return "%s" % (self._commStateString)
242
243         #Returns true if we need to establish an active connection. True for serial connections.
244         def hasActiveConnection(self):
245                 return True
246
247         #Open the active connection to the printer so we can send commands
248         def openActiveConnection(self):
249                 self.closeActiveConnection()
250                 self._thread = threading.Thread(target=self._serialCommunicationThread)
251                 self._thread.daemon = True
252                 self._thread.start()
253
254         #Close the active connection to the printer
255         def closeActiveConnection(self):
256                 if self._process is not None:
257                         self._process.terminate()
258                         self._thread.join()
259
260         #Is the active connection open right now.
261         def isActiveConnectionOpen(self):
262                 if self._process is None:
263                         return False
264                 return self._commState == machineCom.MachineCom.STATE_OPERATIONAL or self._commState == machineCom.MachineCom.STATE_PRINTING or self._commState == machineCom.MachineCom.STATE_PAUSED
265
266         #Are we trying to open an active connection right now.
267         def isActiveConnectionOpening(self):
268                 if self._process is None:
269                         return False
270                 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
271
272         def getTemperature(self, extruder):
273                 if extruder >= len(self._temperature):
274                         return None
275                 return self._temperature[extruder]
276
277         def getBedTemperature(self):
278                 return self._bedTemperature
279
280         #Are we able to send a direct command with sendCommand at this moment in time.
281         def isAbleToSendDirectCommand(self):
282                 return self.isActiveConnectionOpen()
283
284         #Directly send a command to the printer.
285         def sendCommand(self, command):
286                 if self._process is None:
287                         return
288                 self._process.stdin.write('C:%s\n' % (command))
289
290         #Returns true if we got some kind of error. The getErrorLog returns all the information to diagnose the problem.
291         def isInErrorState(self):
292                 return self._commState == machineCom.MachineCom.STATE_ERROR or self._commState == machineCom.MachineCom.STATE_CLOSED_WITH_ERROR
293
294         #Returns the error log in case there was an error.
295         def getErrorLog(self):
296                 return '\n'.join(self._log)
297
298         def _serialCommunicationThread(self):
299                 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
300                         cmdList = [os.path.join(os.path.dirname(sys.executable), 'Cura'), '--serialCommunication']
301                         cmdList += [self._portName + ':' + profile.getMachineSetting('serial_baud')]
302                 else:
303                         cmdList = [sys.executable, '-m', 'Cura.serialCommunication']
304                         cmdList += [self._portName, profile.getMachineSetting('serial_baud')]
305                 if platform.system() == "Darwin":
306                         if platform.machine() == 'i386':
307                                 cmdList = ['arch', '-i386'] + cmdList
308                 self._process = subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
309                 line = self._process.stdout.readline()
310                 while len(line) > 0:
311                         line = line.strip()
312                         line = line.split(':', 1)
313                         if line[0] == '':
314                                 pass
315                         elif line[0] == 'log':
316                                 self._log.append(line[1])
317                                 if len(self._log) > 30:
318                                         self._log.pop(0)
319                         elif line[0] == 'temp':
320                                 line = line[1].split(':')
321                                 self._temperature = json.loads(line[0])
322                                 self._targetTemperature = json.loads(line[1])
323                                 self._bedTemperature = float(line[2])
324                                 self._targetBedTemperature = float(line[3])
325                                 self._doCallback()
326                         elif line[0] == 'message':
327                                 self._doCallback(line[1])
328                         elif line[0] == 'state':
329                                 line = line[1].split(':', 1)
330                                 self._commState = int(line[0])
331                                 self._commStateString = line[1]
332                                 self._doCallback('')
333                         elif line[0] == 'progress':
334                                 self._printProgress = int(line[1])
335                                 self._doCallback()
336                         elif line[0] == 'changeZ':
337                                 self._ZPosition = float(line[1])
338                                 self._doCallback()
339                         else:
340                                 print line
341                         line = self._process.stdout.readline()
342                 self._process = None