chiark / gitweb /
Minor fix for Doodle3D support.
[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 doodle3dConnectionGroup(printerConnectionBase.printerConnectionGroup):
12         PRINTER_LIST_HOST = 'connect.doodle3d.com'
13         PRINTER_LIST_PATH = '/api/list.php'
14
15         def __init__(self):
16                 super(doodle3dConnectionGroup, self).__init__("Doodle3D")
17                 self._http = None
18                 self._host = self.PRINTER_LIST_HOST
19                 self._connectionMap = {}
20
21                 self._thread = threading.Thread(target=self._doodle3DThread)
22                 self._thread.daemon = True
23                 self._thread.start()
24
25         def getAvailableConnections(self):
26                 return filter(lambda c: c.isAvailable(), self._connectionMap.values())
27
28         def remove(self, host):
29                 del self._connectionMap[host]
30
31         def getIconID(self):
32                 return 27
33
34         def getPriority(self):
35                 return 100
36
37         def __cmp__(self, other):
38                 return self.getPriority() - other.getPriority()
39
40         def __repr__(self):
41                 return self.name
42
43         def _doodle3DThread(self):
44                 self._waitDelay = 0
45                 while True:
46                         printerList = self._request('GET', self.PRINTER_LIST_PATH)
47                         if not printerList or type(printerList) is not dict or 'data' not in printerList or type(printerList['data']) is not list:
48                                 #Check if we are connected to the Doodle3D box in access point mode, as this gives an
49                                 # invalid reply on the printer list API
50                                 printerList = {'data': [{'localip': 'draw.doodle3d.com'}]}
51
52                         #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.
53                         # (connect.doodle3d.com also checks for this IP in the javascript code)
54                         printerList['data'].append({'localip': '192.168.5.1'})
55
56                         #Check the status of each possible IP, if we find a valid box with a printer connected. Use that IP.
57                         for possiblePrinter in printerList['data']:
58                                 if possiblePrinter['localip'] not in self._connectionMap:
59                                         status = self._request('GET', '/d3dapi/config/?network.cl.wifiboxid=', host=possiblePrinter['localip'])
60                                         if status and 'data' in status and 'network.cl.wifiboxid' in status['data']:
61                                                 self._connectionMap[possiblePrinter['localip']] = doodle3dConnect(possiblePrinter['localip'], status['data']['network.cl.wifiboxid'], self)
62
63                         # Delay a bit more after every request. This so we do not stress the connect.doodle3d.com api too much
64                         if self._waitDelay < 10:
65                                 self._waitDelay += 1
66                         time.sleep(self._waitDelay * 60)
67
68         def _request(self, method, path, postData = None, host = None):
69                 if host is None:
70                         host = self._host
71                 if self._http is None or self._http.host != host:
72                         self._http = httpclient.HTTPConnection(host, timeout=30)
73
74                 try:
75                         if postData is not None:
76                                 self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
77                         else:
78                                 self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
79                 except:
80                         self._http.close()
81                         return None
82                 try:
83                         response = self._http.getresponse()
84                         responseText = response.read()
85                 except:
86                         self._http.close()
87                         return None
88                 try:
89                         response = json.loads(responseText)
90                 except ValueError:
91                         self._http.close()
92                         return None
93                 if response['status'] != 'success':
94                         return False
95
96                 return response
97
98 #Class to connect and print files with the doodle3d.com wifi box
99 # Auto-detects if the Doodle3D box is available with a printer
100 class doodle3dConnect(printerConnectionBase.printerConnectionBase):
101         def __init__(self, host, name, group):
102                 super(doodle3dConnect, self).__init__(name)
103
104                 self._http = None
105                 self._group = group
106                 self._host = host
107
108                 self._isAvailable = False
109                 self._printing = False
110                 self._fileBlocks = []
111                 self._commandList = []
112                 self._blockIndex = None
113                 self._lineCount = 0
114                 self._progressLine = 0
115                 self._hotendTemperature = [None] * 4
116                 self._bedTemperature = None
117                 self._errorCount = 0
118                 self._interruptSleep = False
119
120                 self.checkThread = threading.Thread(target=self._doodle3DThread)
121                 self.checkThread.daemon = True
122                 self.checkThread.start()
123
124         #Load the file into memory for printing.
125         def loadFile(self, filename):
126                 if self._printing:
127                         return False
128                 self._fileBlocks = []
129                 self._lineCount = 0
130                 block = []
131                 blockSize = 0
132                 f = open(filename, "r")
133                 for line in f:
134                         #Strip out comments, we do not need to send comments
135                         if ';' in line:
136                                 line = line[:line.index(';')]
137                         #Strip out whitespace at the beginning/end this saves data to send.
138                         line = line.strip()
139
140                         if len(line) < 1:
141                                 continue
142                         self._lineCount += 1
143                         #Put the lines in 8k sized blocks, so we can send those blocks as http requests.
144                         if blockSize + len(line) > 1024 * 8:
145                                 self._fileBlocks.append('\n'.join(block) + '\n')
146                                 block = []
147                                 blockSize = 0
148                         blockSize += len(line) + 1
149                         block.append(line)
150                 self._fileBlocks.append('\n'.join(block) + '\n')
151                 f.close()
152                 self._doCallback()
153                 return True
154
155         #Start printing the previously loaded file
156         def startPrint(self):
157                 if self._printing or len(self._fileBlocks) < 1:
158                         return
159                 self._progressLine = 0
160                 self._blockIndex = 0
161                 self._printing = True
162                 self._interruptSleep = True
163
164         #Abort the previously loaded print file
165         def cancelPrint(self):
166                 if not self._printing:
167                         return
168                 if self._request('POST', '/d3dapi/printer/stop', {'gcode': 'M104 S0\nG28'}):
169                         self._printing = False
170
171         def isPrinting(self):
172                 return self._printing
173
174         #Amount of progression of the current print file. 0.0 to 1.0
175         def getPrintProgress(self):
176                 if self._lineCount < 1:
177                         return 0.0
178                 return float(self._progressLine) / float(self._lineCount)
179
180         # Return if the printer with this connection type is available
181         def isAvailable(self):
182                 return self._isAvailable
183
184         #Are we able to send a direct coammand with sendCommand at this moment in time.
185         def isAbleToSendDirectCommand(self):
186                 #The delay on direct commands is very high and so we disabled it.
187                 return False #self._isAvailable and not self._printing
188
189         #Directly send a command to the printer.
190         def sendCommand(self, command):
191                 if not self._isAvailable or self._printing:
192                         return
193                 self._commandList.append(command)
194                 self._interruptSleep = True
195
196         # Get the connection status string. This is displayed to the user and can be used to communicate
197         #  various information to the user.
198         def getStatusString(self):
199                 if not self._isAvailable:
200                         return "Doodle3D box not found"
201                 if self._printing:
202                         ret = "Print progress: %.1f%%" % (self.getPrintProgress() * 100.0)
203                         if self._blockIndex < len(self._fileBlocks):
204                                 ret += "\nSending GCode: %.1f%%" % (float(self._blockIndex) * 100.0 / float(len(self._fileBlocks)))
205                         elif len(self._fileBlocks) > 0:
206                                 ret += "\nFinished sending GCode to Doodle3D box.\nPrint will continue even if you shut down Cura."
207                         else:
208                                 ret += "\nDifferent print still running..."
209                         ret += "\nErrorCount: %d" % (self._errorCount)
210                         return ret
211                 return "Printer found, waiting for print command."
212
213         #Get the temperature of an extruder, returns None is no temperature is known for this extruder
214         def getTemperature(self, extruder):
215                 return self._hotendTemperature[extruder]
216
217         #Get the temperature of the heated bed, returns None is no temperature is known for the heated bed
218         def getBedTemperature(self):
219                 return self._bedTemperature
220
221         def _doodle3DThread(self):
222                 while True:
223                         stateReply = self._request('GET', '/d3dapi/info/status')
224                         if stateReply is None or not stateReply:
225                                 # No API, wait 5 seconds before looking for Doodle3D again.
226                                 # API gave back an error (this can happen if the Doodle3D box is connecting to the printer)
227                                 # The Doodle3D box could also be offline, if we reach a high enough errorCount then assume the box is gone.
228                                 self._errorCount += 1
229                                 if self._errorCount > 10:
230                                         if self._isAvailable:
231                                                 self._printing = False
232                                                 self._isAvailable = False
233                                                 self._doCallback()
234                                         self._sleep(15)
235                                         self._group.remove(self._host)
236                                         return
237                                 else:
238                                         self._sleep(3)
239                                 continue
240                         if stateReply['data']['state'] == 'disconnected':
241                                 # No printer connected, we do not have a printer available, but the Doodle3D box is there.
242                                 # So keep trying to find a printer connected to it.
243                                 if self._isAvailable:
244                                         self._printing = False
245                                         self._isAvailable = False
246                                         self._doCallback()
247                                 self._sleep(15)
248                                 continue
249                         self._errorCount = 0
250
251                         #We got a valid status, set the doodle3d printer as available.
252                         if not self._isAvailable:
253                                 self._isAvailable = True
254
255                         if 'hotend' in stateReply['data']:
256                                 self._hotendTemperature[0] = stateReply['data']['hotend']
257                         if 'bed' in stateReply['data']:
258                                 self._bedTemperature = stateReply['data']['bed']
259
260                         if stateReply['data']['state'] == 'idle' or stateReply['data']['state'] == 'buffering':
261                                 if self._printing:
262                                         if self._blockIndex < len(self._fileBlocks):
263                                                 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex], 'start': 'True', 'first': 'True'}):
264                                                         self._blockIndex += 1
265                                                 else:
266                                                         self._sleep(1)
267                                         else:
268                                                 self._printing = False
269                                 else:
270                                         if len(self._commandList) > 0:
271                                                 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._commandList[0], 'start': 'True', 'first': 'True'}):
272                                                         self._commandList.pop(0)
273                                                 else:
274                                                         self._sleep(1)
275                                         else:
276                                                 self._sleep(5)
277                         elif stateReply['data']['state'] == 'printing':
278                                 if self._printing:
279                                         if self._blockIndex < len(self._fileBlocks):
280                                                 for n in xrange(0, 5):
281                                                         if self._blockIndex < len(self._fileBlocks):
282                                                                 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex]}):
283                                                                         self._blockIndex += 1
284                                                                 else:
285                                                                         #Cannot send new block, wait a bit, so we do not overload the API
286                                                                         self._sleep(15)
287                                                                         break
288                                         else:
289                                                 #If we are no longer sending new GCode delay a bit so we request the status less often.
290                                                 self._sleep(5)
291                                         if 'current_line' in stateReply['data']:
292                                                 self._progressLine = stateReply['data']['current_line']
293                                 else:
294                                         #Got a printing state without us having send the print file, set the state to printing, but make sure we never send anything.
295                                         if 'current_line' in stateReply['data'] and 'total_lines' in stateReply['data'] and stateReply['data']['total_lines'] > 2:
296                                                 self._printing = True
297                                                 self._fileBlocks = []
298                                                 self._blockIndex = 1
299                                                 self._progressLine = stateReply['data']['current_line']
300                                                 self._lineCount = stateReply['data']['total_lines']
301                                         self._sleep(5)
302                         self._doCallback()
303
304         def _sleep(self, timeOut):
305                 while timeOut > 0.0:
306                         if not self._interruptSleep:
307                                 time.sleep(0.1)
308                         timeOut -= 0.1
309                 self._interruptSleep = False
310
311         def _request(self, method, path, postData = None, host = None):
312                 if host is None:
313                         host = self._host
314                 if self._http is None or self._http.host != host:
315                         self._http = httpclient.HTTPConnection(host, timeout=30)
316
317                 try:
318                         if postData is not None:
319                                 self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
320                         else:
321                                 self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
322                 except:
323                         self._http.close()
324                         return None
325                 try:
326                         response = self._http.getresponse()
327                         responseText = response.read()
328                 except:
329                         self._http.close()
330                         return None
331                 try:
332                         response = json.loads(responseText)
333                 except ValueError:
334                         self._http.close()
335                         return None
336                 if response['status'] != 'success':
337                         return False
338
339                 return response
340
341 if __name__ == '__main__':
342         d = doodle3dConnect()
343         print 'Searching for Doodle3D box'
344         while not d.isAvailable():
345                 time.sleep(1)
346
347         while d.isPrinting():
348                 print 'Doodle3D already printing! Requesting stop!'
349                 d.cancelPrint()
350                 time.sleep(5)
351
352         print 'Doodle3D box found, printing!'
353         d.loadFile("C:/Models/belt-tensioner-wave_export.gcode")
354         d.startPrint()
355         while d.isPrinting() and d.isAvailable():
356                 time.sleep(1)
357                 print d.getTemperature(0), d.getStatusString(), d.getPrintProgress(), d._progressLine, d._lineCount, d._blockIndex, len(d._fileBlocks)
358         print 'Done'