chiark / gitweb /
Handle the doodle3d api better, which can be slow if it gets a lot of data.
[cura.git] / Cura / util / printerConnection / doodle3dConnect.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2
3 import threading
4 import json
5 import httplib as httpclient
6 import urllib
7 import time
8
9 from Cura.util.printerConnection import printerConnectionBase
10
11 #Class to connect and print files with the doodle3d.com wifi box
12 # Auto-detects if the Doodle3D box is available with a printer
13 class doodle3dConnect(printerConnectionBase.printerConnectionBase):
14         PRINTER_LIST_HOST = 'connect.doodle3d.com'
15         PRINTER_LIST_PATH = '/api/list.php'
16
17         def __init__(self):
18                 super(doodle3dConnect, self).__init__()
19
20                 self._http = None
21                 self._host = None
22                 self._isAvailable = False
23                 self._printing = False
24                 self._fileBlocks = []
25                 self._commandList = []
26                 self._blockIndex = None
27                 self._lineCount = 0
28                 self._progressLine = 0
29                 self._hotendTemperature = [None] * 4
30                 self._bedTemperature = None
31                 self._errorCount = 0
32
33                 self.checkThread = threading.Thread(target=self._doodle3DThread)
34                 self.checkThread.daemon = True
35                 self.checkThread.start()
36
37         #Load the file into memory for printing.
38         def loadFile(self, filename):
39                 if self._printing:
40                         return False
41                 self._fileBlocks = []
42                 self._lineCount = 0
43                 block = []
44                 blockSize = 0
45                 f = open(filename, "r")
46                 for line in f:
47                         #Strip out comments, we do not need to send comments
48                         if ';' in line:
49                                 line = line[:line.index(';')]
50                         #Strip out whitespace at the beginning/end this saves data to send.
51                         line = line.strip()
52
53                         if len(line) < 1:
54                                 continue
55                         self._lineCount += 1
56                         #Put the lines in 8k sized blocks, so we can send those blocks as http requests.
57                         if blockSize + len(line) > 1024 * 8:
58                                 self._fileBlocks.append('\n'.join(block) + '\n')
59                                 block = []
60                                 blockSize = 0
61                         blockSize += len(line) + 1
62                         block.append(line)
63                 self._fileBlocks.append('\n'.join(block) + '\n')
64                 f.close()
65                 self._doCallback()
66                 return True
67
68         #Start printing the previously loaded file
69         def startPrint(self):
70                 if self._printing or len(self._fileBlocks) < 1:
71                         return
72                 self._progressLine = 0
73                 self._blockIndex = 0
74                 self._printing = True
75
76         #Abort the previously loaded print file
77         def cancelPrint(self):
78                 if not self._printing:
79                         return
80                 if self._request('POST', '/d3dapi/printer/stop', {'gcode': 'M104 S0\nG28'}):
81                         self._printing = False
82
83         def isPrinting(self):
84                 return self._printing
85
86         #Amount of progression of the current print file. 0.0 to 1.0
87         def getPrintProgress(self):
88                 if self._lineCount < 1:
89                         return 0.0
90                 return float(self._progressLine) / float(self._lineCount)
91
92         # Return if the printer with this connection type is available
93         def isAvailable(self):
94                 return self._isAvailable
95
96         #Are we able to send a direct coammand with sendCommand at this moment in time.
97         def isAbleToSendDirectCommand(self):
98                 #The delay on direct commands is very high and so we disabled it.
99                 return False #self._isAvailable and not self._printing
100
101         #Directly send a command to the printer.
102         def sendCommand(self, command):
103                 if not self._isAvailable or self._printing:
104                         return
105                 self._commandList.append(command)
106
107         # Get the connection status string. This is displayed to the user and can be used to communicate
108         #  various information to the user.
109         def getStatusString(self):
110                 if not self._isAvailable:
111                         return "Doodle3D box not found"
112                 if self._printing:
113                         ret = "Print progress: %.1f%%" % (self.getPrintProgress() * 100.0)
114                         if self._blockIndex < len(self._fileBlocks):
115                                 ret += "\nSending GCode: %.1f%%" % (float(self._blockIndex) * 100.0 / float(len(self._fileBlocks)))
116                         elif len(self._fileBlocks) > 0:
117                                 ret += "\nFinished sending GCode to Doodle3D box.\nPrint will continue even if you shut down Cura."
118                         else:
119                                 ret += "\nDifferent print still running..."
120                         ret += "\nErrorCount: %d" % (self._errorCount)
121                         return ret
122                 return "Printer found, waiting for print command."
123
124         #Get the temperature of an extruder, returns None is no temperature is known for this extruder
125         def getTemperature(self, extruder):
126                 return self._hotendTemperature[extruder]
127
128         #Get the temperature of the heated bed, returns None is no temperature is known for the heated bed
129         def getBedTemperature(self):
130                 return self._bedTemperature
131
132         def _doodle3DThread(self):
133                 waitDelay = 0
134                 while True:
135                         while self._host is None:
136                                 printerList = self._request('GET', self.PRINTER_LIST_PATH, host=self.PRINTER_LIST_HOST)
137                                 if not printerList or type(printerList) is not dict or 'data' not in printerList or type(printerList['data']) is not list:
138                                         #Check if we are connected to the Doodle3D box in access point mode, as this gives an
139                                         # invalid reply on the printer list API
140                                         printerList = {'data': [{'localip': 'draw.doodle3d.com'}]}
141
142                                 #Add the 192.168.5.1 IP to the list of printers to check, as this is the LAN port IP, which could also be available.
143                                 # (connect.doodle3d.com also checks for this IP in the javascript code)
144                                 printerList['data'].append({'localip': '192.168.5.1'})
145
146                                 #Check the status of each possible IP, if we find a valid box with a printer connected. Use that IP.
147                                 for possiblePrinter in printerList['data']:
148                                         status = self._request('GET', '/d3dapi/info/status', host=possiblePrinter['localip'])
149                                         if status and 'data' in status:
150                                                 self._host = possiblePrinter['localip']
151                                                 break
152
153                                 if self._host is None:
154                                         #If we cannot find a doodle3d box, delay a minute and request the list again.
155                                         # This so we do not stress the connect.doodle3d.com api too much
156                                         if waitDelay < 10:
157                                                 waitDelay += 1
158                                         time.sleep(waitDelay * 60)
159                                 else:
160                                         #If we found a doodle3D box, reset the wait delay, so we can find it again in case it gets lost
161                                         self._errorCount = 0
162                                         waitDelay = 0
163
164                         stateReply = self._request('GET', '/d3dapi/info/status')
165                         if stateReply is None or not stateReply:
166                                 # No API, wait 15 seconds before looking for Doodle3D again.
167                                 # API gave back an error (this can happen if the Doodle3D box is connecting to the printer)
168                                 self._errorCount += 1
169                                 if self._errorCount > 10:
170                                         self._host = None
171                                         if self._isAvailable:
172                                                 self._printing = False
173                                                 self._isAvailable = False
174                                                 self._doCallback()
175                                 time.sleep(15)
176                                 continue
177                         if stateReply['data']['state'] == 'disconnected':
178                                 # No printer connected
179                                 if self._isAvailable:
180                                         self._printing = False
181                                         self._isAvailable = False
182                                         self._doCallback()
183                                 time.sleep(5)
184                                 continue
185                         self._errorCount = 0
186
187                         #We got a valid status, set the doodle3d printer as available.
188                         if not self._isAvailable:
189                                 self._isAvailable = True
190
191                         if 'hotend' in stateReply['data']:
192                                 self._hotendTemperature[0] = stateReply['data']['hotend']
193                         if 'bed' in stateReply['data']:
194                                 self._bedTemperature = stateReply['data']['bed']
195
196                         if stateReply['data']['state'] == 'idle' or stateReply['data']['state'] == 'buffering':
197                                 if self._printing:
198                                         if self._blockIndex < len(self._fileBlocks):
199                                                 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex], 'start': 'True', 'first': 'True'}):
200                                                         self._blockIndex += 1
201                                                 else:
202                                                         time.sleep(1)
203                                         else:
204                                                 self._printing = False
205                                 else:
206                                         if len(self._commandList) > 0:
207                                                 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._commandList[0], 'start': 'True', 'first': 'True'}):
208                                                         self._commandList.pop(0)
209                                                 else:
210                                                         time.sleep(1)
211                                         else:
212                                                 time.sleep(5)
213                         elif stateReply['data']['state'] == 'printing':
214                                 if self._printing:
215                                         if self._blockIndex < len(self._fileBlocks):
216                                                 for n in xrange(0, 5):
217                                                         if self._blockIndex < len(self._fileBlocks):
218                                                                 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex]}):
219                                                                         self._blockIndex += 1
220                                                                 else:
221                                                                         #Cannot send new block, wait a bit, so we do not overload the API
222                                                                         time.sleep(15)
223                                                                         break
224                                         else:
225                                                 #If we are no longer sending new GCode delay a bit so we request the status less often.
226                                                 time.sleep(5)
227                                         if 'current_line' in stateReply['data']:
228                                                 self._progressLine = stateReply['data']['current_line']
229                                 else:
230                                         #Got a printing state without us having send the print file, set the state to printing, but make sure we never send anything.
231                                         if 'current_line' in stateReply['data'] and 'total_lines' in stateReply['data'] and stateReply['data']['total_lines'] > 2:
232                                                 self._printing = True
233                                                 self._fileBlocks = []
234                                                 self._blockIndex = 1
235                                                 self._progressLine = stateReply['data']['current_line']
236                                                 self._lineCount = stateReply['data']['total_lines']
237                                         time.sleep(5)
238                         self._doCallback()
239
240         def _request(self, method, path, postData = None, host = None):
241                 if host is None:
242                         host = self._host
243                 if self._http is None or self._http.host != host:
244                         self._http = httpclient.HTTPConnection(host, timeout=30)
245
246                 try:
247                         if postData is not None:
248                                 self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
249                         else:
250                                 self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
251                 except:
252                         self._http.close()
253                         return None
254                 try:
255                         response = self._http.getresponse()
256                         responseText = response.read()
257                 except:
258                         self._http.close()
259                         return None
260                 try:
261                         response = json.loads(responseText)
262                 except ValueError:
263                         self._http.close()
264                         return None
265                 if response['status'] != 'success':
266                         return False
267
268                 return response
269
270 if __name__ == '__main__':
271         d = doodle3dConnect()
272         print 'Searching for Doodle3D box'
273         while not d.isAvailable():
274                 time.sleep(1)
275
276         while d.isPrinting():
277                 print 'Doodle3D already printing! Requesting stop!'
278                 d.cancelPrint()
279                 time.sleep(5)
280
281         print 'Doodle3D box found, printing!'
282         d.loadFile("C:/Models/belt-tensioner-wave_export.gcode")
283         d.startPrint()
284         while d.isPrinting() and d.isAvailable():
285                 time.sleep(1)
286                 print d.getTemperature(0), d.getStatusString(), d.getPrintProgress(), d._progressLine, d._lineCount, d._blockIndex, len(d._fileBlocks)
287         print 'Done'