chiark / gitweb /
Disable motors when a print is canceled. Follow on to #118
[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
15 from Cura.util import profile
16 from Cura.util import machineCom
17 from Cura.util.printerConnection import printerConnectionBase
18
19 class serialConnectionGroup(printerConnectionBase.printerConnectionGroup):
20         """
21         The serial connection group. Keeps track of all available serial ports,
22         and builds a serialConnection for each port.
23         """
24         def __init__(self):
25                 super(serialConnectionGroup, self).__init__("USB")
26                 self._connectionMap = {}
27
28         def getAvailableConnections(self):
29                 if profile.getMachineSetting('serial_port') == 'AUTO':
30                         serialList = machineCom.serialList(True)
31                 else:
32                         serialList = [profile.getMachineSetting('serial_port')]
33                 for port in serialList:
34                         if port not in self._connectionMap:
35                                 self._connectionMap[port] = serialConnection(port)
36                 for key in self._connectionMap.keys():
37                         if key not in serialList and not self._connectionMap[key].isActiveConnectionOpen():
38                                 self._connectionMap.pop(key)
39                 return self._connectionMap.values()
40
41         def getIconID(self):
42                 return 6
43
44         def getPriority(self):
45                 return 50
46
47 class serialConnection(printerConnectionBase.printerConnectionBase):
48         """
49         A serial connection. Needs to build an active-connection.
50         When an active connection is created, a 2nd python process is spawned which handles the actual serial communication.
51
52         This class communicates with the Cura.serialCommunication module trough stdin/stdout pipes.
53         """
54         def __init__(self, port):
55                 super(serialConnection, self).__init__(port)
56                 self._portName = port
57
58                 self._process = None
59                 self._thread = None
60
61                 self._temperature = []
62                 self._targetTemperature = []
63                 self._bedTemperature = 0
64                 self._targetBedTemperature = 0
65                 self._log = []
66
67                 self._commState = None
68                 self._commStateString = None
69                 self._gcodeData = []
70
71         #Load the data into memory for printing, returns True on success
72         def loadGCodeData(self, dataStream):
73                 if self.isPrinting() is None:
74                         return False
75                 self._gcodeData = []
76                 for line in dataStream:
77                         #Strip out comments, we do not need to send comments
78                         if ';' in line:
79                                 line = line[:line.index(';')]
80                         #Strip out whitespace at the beginning/end this saves data to send.
81                         line = line.strip()
82
83                         if len(line) < 1:
84                                 continue
85                         self._gcodeData.append(line)
86                 return True
87
88         #Start printing the previously loaded file
89         def startPrint(self):
90                 if self.isPrinting() or len(self._gcodeData) < 1 or self._process is None:
91                         return
92                 self._process.stdin.write('STOP\n')
93                 for line in self._gcodeData:
94                         self._process.stdin.write('G:%s\n' % (line))
95                 self._process.stdin.write('START\n')
96                 self._printProgress = 0
97
98         def coolDown(self):
99                 cooldown_toolhead = "M104 S0"
100                 for i in range(0,3):
101                         change_toolhead = "T%d".format(i)
102                         self.sendCommand(change_toolhead)
103                         self.sendCommand(cooldown_toolhead)
104                 self.sendCommand("M140 S0") #Bed
105
106         def disableSteppers(self):
107                 self.sendCommand("M18")
108
109         #Abort the previously loaded print file
110         def cancelPrint(self):
111                 if not self.isPrinting() or self._process is None:
112                         return
113                 self._process.stdin.write('STOP\n')
114                 self._printProgress = 0
115                 self.coolDown()
116                 self.disableSteppers()
117
118         def isPrinting(self):
119                 return self._commState == machineCom.MachineCom.STATE_PRINTING or self.isPaused()
120
121         #Returns true if we have the ability to pause the file printing.
122         def hasPause(self):
123                 return True
124
125         def isPaused(self):
126                 return self._commState == machineCom.MachineCom.STATE_PAUSED
127
128         #Pause or unpause the printing depending on the value, if supported.
129         def pause(self, value):
130                 if not self.isPrinting() or self._process is None:
131                         return
132                 self._process.stdin.write('PAUSE\n' if value else "RESUME\n")
133
134         #Amount of progression of the current print file. 0.0 to 1.0
135         def getPrintProgress(self):
136                 if len(self._gcodeData) < 1:
137                         return 0.0
138                 return float(self._printProgress) / float(len(self._gcodeData))
139
140         # Return if the printer with this connection type is available
141         def isAvailable(self):
142                 return True
143
144         # Get the connection status string. This is displayed to the user and can be used to communicate
145         #  various information to the user.
146         def getStatusString(self):
147                 return "%s" % (self._commStateString)
148
149         #Returns true if we need to establish an active connection. True for serial connections.
150         def hasActiveConnection(self):
151                 return True
152
153         #Open the active connection to the printer so we can send commands
154         def openActiveConnection(self):
155                 self.closeActiveConnection()
156                 self._thread = threading.Thread(target=self._serialCommunicationThread)
157                 self._thread.daemon = True
158                 self._thread.start()
159
160         #Close the active connection to the printer
161         def closeActiveConnection(self):
162                 if self._process is not None:
163                         self._process.terminate()
164                         self._thread.join()
165
166         #Is the active connection open right now.
167         def isActiveConnectionOpen(self):
168                 if self._process is None:
169                         return False
170                 return self._commState == machineCom.MachineCom.STATE_OPERATIONAL or self._commState == machineCom.MachineCom.STATE_PRINTING or self._commState == machineCom.MachineCom.STATE_PAUSED
171
172         #Are we trying to open an active connection right now.
173         def isActiveConnectionOpening(self):
174                 if self._process is None:
175                         return False
176                 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
177
178         def getTemperature(self, extruder):
179                 if extruder >= len(self._temperature):
180                         return None
181                 return self._temperature[extruder]
182
183         def getBedTemperature(self):
184                 return self._bedTemperature
185
186         #Are we able to send a direct command with sendCommand at this moment in time.
187         def isAbleToSendDirectCommand(self):
188                 return self.isActiveConnectionOpen()
189
190         #Directly send a command to the printer.
191         def sendCommand(self, command):
192                 if self._process is None:
193                         return
194                 self._process.stdin.write('C:%s\n' % (command))
195
196         #Returns true if we got some kind of error. The getErrorLog returns all the information to diagnose the problem.
197         def isInErrorState(self):
198                 return self._commState == machineCom.MachineCom.STATE_ERROR or self._commState == machineCom.MachineCom.STATE_CLOSED_WITH_ERROR
199
200         #Returns the error log in case there was an error.
201         def getErrorLog(self):
202                 return '\n'.join(self._log)
203
204         def _serialCommunicationThread(self):
205                 if platform.system() == "Darwin" and hasattr(sys, 'frozen'):
206                         cmdList = [os.path.join(os.path.dirname(sys.executable), 'Cura'), '--serialCommunication']
207                         cmdList += [self._portName + ':' + profile.getMachineSetting('serial_baud')]
208                 else:
209                         cmdList = [sys.executable, '-m', 'Cura.serialCommunication']
210                         cmdList += [self._portName, profile.getMachineSetting('serial_baud')]
211                 if platform.system() == "Darwin":
212                         if platform.machine() == 'i386':
213                                 cmdList = ['arch', '-i386'] + cmdList
214                 self._process = subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
215                 line = self._process.stdout.readline()
216                 while len(line) > 0:
217                         line = line.strip()
218                         line = line.split(':', 1)
219                         if line[0] == '':
220                                 pass
221                         elif line[0] == 'log':
222                                 self._log.append(line[1])
223                                 if len(self._log) > 30:
224                                         self._log.pop(0)
225                         elif line[0] == 'temp':
226                                 line = line[1].split(':')
227                                 self._temperature = json.loads(line[0])
228                                 self._targetTemperature = json.loads(line[1])
229                                 self._bedTemperature = float(line[2])
230                                 self._targetBedTemperature = float(line[3])
231                                 self._doCallback()
232                         elif line[0] == 'message':
233                                 self._doCallback(line[1])
234                         elif line[0] == 'state':
235                                 line = line[1].split(':', 1)
236                                 self._commState = int(line[0])
237                                 self._commStateString = line[1]
238                                 self._doCallback('')
239                         elif line[0] == 'progress':
240                                 self._printProgress = int(line[1])
241                                 self._doCallback()
242                         else:
243                                 print line
244                         line = self._process.stdout.readline()
245                 self._process = None