1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
5 import httplib as httpclient
9 from Cura.util.printerConnection import printerConnectionBase
11 class doodle3dConnectionGroup(printerConnectionBase.printerConnectionGroup):
12 PRINTER_LIST_HOST = 'connect.doodle3d.com'
13 PRINTER_LIST_PATH = '/api/list.php'
16 super(doodle3dConnectionGroup, self).__init__("Doodle3D")
18 self._host = self.PRINTER_LIST_HOST
19 self._connectionMap = {}
21 self._thread = threading.Thread(target=self._doodle3DThread)
22 self._thread.daemon = True
25 def getAvailableConnections(self):
26 return filter(lambda c: c.isAvailable(), self._connectionMap.values())
28 def remove(self, host):
29 del self._connectionMap[host]
34 def getPriority(self):
37 def __cmp__(self, other):
38 return self.getPriority() - other.getPriority()
43 def _doodle3DThread(self):
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'}]}
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'})
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)
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:
66 time.sleep(self._waitDelay * 60)
68 def _request(self, method, path, postData = None, host = None):
71 if self._http is None or self._http.host != host:
72 self._http = httpclient.HTTPConnection(host, timeout=30)
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"})
78 self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
83 response = self._http.getresponse()
84 responseText = response.read()
89 response = json.loads(responseText)
93 if response['status'] != 'success':
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)
108 self._isAvailable = False
109 self._printing = False
110 self._fileBlocks = []
111 self._commandList = []
112 self._blockIndex = None
114 self._progressLine = 0
115 self._hotendTemperature = [None] * 4
116 self._bedTemperature = None
118 self._interruptSleep = False
120 self.checkThread = threading.Thread(target=self._doodle3DThread)
121 self.checkThread.daemon = True
122 self.checkThread.start()
124 #Load the file into memory for printing.
125 def loadGCodeData(self, dataStream):
128 self._fileBlocks = []
132 for line in dataStream:
133 #Strip out comments, we do not need to send comments
135 line = line[:line.index(';')]
136 #Strip out whitespace at the beginning/end this saves data to send.
142 #Put the lines in 8k sized blocks, so we can send those blocks as http requests.
143 if blockSize + len(line) > 1024 * 8:
144 self._fileBlocks.append('\n'.join(block) + '\n')
147 blockSize += len(line) + 1
149 self._fileBlocks.append('\n'.join(block) + '\n')
153 #Start printing the previously loaded file
154 def startPrint(self):
155 if self._printing or len(self._fileBlocks) < 1:
157 self._progressLine = 0
159 self._printing = True
160 self._interruptSleep = True
162 #Abort the previously loaded print file
163 def cancelPrint(self):
164 if not self._printing:
166 if self._request('POST', '/d3dapi/printer/stop', {'gcode': 'M104 S0\nG28'}):
167 self._printing = False
169 def isPrinting(self):
170 return self._printing
172 #Amount of progression of the current print file. 0.0 to 1.0
173 def getPrintProgress(self):
174 if self._lineCount < 1:
176 return float(self._progressLine) / float(self._lineCount)
178 # Return if the printer with this connection type is available
179 def isAvailable(self):
180 return self._isAvailable
182 #Are we able to send a direct coammand with sendCommand at this moment in time.
183 def isAbleToSendDirectCommand(self):
184 #The delay on direct commands is very high and so we disabled it.
185 return False #self._isAvailable and not self._printing
187 #Directly send a command to the printer.
188 def sendCommand(self, command):
189 if not self._isAvailable or self._printing:
191 self._commandList.append(command)
192 self._interruptSleep = True
194 # Get the connection status string. This is displayed to the user and can be used to communicate
195 # various information to the user.
196 def getStatusString(self):
197 if not self._isAvailable:
198 return "Doodle3D box not found"
200 ret = "Print progress: %.1f%%" % (self.getPrintProgress() * 100.0)
201 if self._blockIndex < len(self._fileBlocks):
202 ret += "\nSending GCode: %.1f%%" % (float(self._blockIndex) * 100.0 / float(len(self._fileBlocks)))
203 elif len(self._fileBlocks) > 0:
204 ret += "\nFinished sending GCode to Doodle3D box.\nPrint will continue even if you shut down Cura."
206 ret += "\nDifferent print still running..."
207 ret += "\nErrorCount: %d" % (self._errorCount)
209 return "Printer found, waiting for print command."
211 #Get the temperature of an extruder, returns None is no temperature is known for this extruder
212 def getTemperature(self, extruder):
213 return self._hotendTemperature[extruder]
215 #Get the temperature of the heated bed, returns None is no temperature is known for the heated bed
216 def getBedTemperature(self):
217 return self._bedTemperature
219 def _doodle3DThread(self):
221 stateReply = self._request('GET', '/d3dapi/info/status')
222 if stateReply is None or not stateReply:
223 # No API, wait 5 seconds before looking for Doodle3D again.
224 # API gave back an error (this can happen if the Doodle3D box is connecting to the printer)
225 # The Doodle3D box could also be offline, if we reach a high enough errorCount then assume the box is gone.
226 self._errorCount += 1
227 if self._errorCount > 10:
228 if self._isAvailable:
229 self._printing = False
230 self._isAvailable = False
233 self._group.remove(self._host)
238 if stateReply['data']['state'] == 'disconnected':
239 # No printer connected, we do not have a printer available, but the Doodle3D box is there.
240 # So keep trying to find a printer connected to it.
241 if self._isAvailable:
242 self._printing = False
243 self._isAvailable = False
249 #We got a valid status, set the doodle3d printer as available.
250 if not self._isAvailable:
251 self._isAvailable = True
253 if 'hotend' in stateReply['data']:
254 self._hotendTemperature[0] = stateReply['data']['hotend']
255 if 'bed' in stateReply['data']:
256 self._bedTemperature = stateReply['data']['bed']
258 if stateReply['data']['state'] == 'idle' or stateReply['data']['state'] == 'buffering':
260 if self._blockIndex < len(self._fileBlocks):
261 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex], 'start': 'True', 'first': 'True'}):
262 self._blockIndex += 1
266 self._printing = False
268 if len(self._commandList) > 0:
269 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._commandList[0], 'start': 'True', 'first': 'True'}):
270 self._commandList.pop(0)
275 elif stateReply['data']['state'] == 'printing':
277 if self._blockIndex < len(self._fileBlocks):
278 for n in xrange(0, 5):
279 if self._blockIndex < len(self._fileBlocks):
280 if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex]}):
281 self._blockIndex += 1
283 #Cannot send new block, wait a bit, so we do not overload the API
287 #If we are no longer sending new GCode delay a bit so we request the status less often.
289 if 'current_line' in stateReply['data']:
290 self._progressLine = stateReply['data']['current_line']
292 #Got a printing state without us having send the print file, set the state to printing, but make sure we never send anything.
293 if 'current_line' in stateReply['data'] and 'total_lines' in stateReply['data'] and stateReply['data']['total_lines'] > 2:
294 self._printing = True
295 self._fileBlocks = []
297 self._progressLine = stateReply['data']['current_line']
298 self._lineCount = stateReply['data']['total_lines']
302 def _sleep(self, timeOut):
304 if not self._interruptSleep:
307 self._interruptSleep = False
309 def _request(self, method, path, postData = None, host = None):
312 if self._http is None or self._http.host != host:
313 self._http = httpclient.HTTPConnection(host, timeout=30)
316 if postData is not None:
317 self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
319 self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
324 response = self._http.getresponse()
325 responseText = response.read()
330 response = json.loads(responseText)
334 if response['status'] != 'success':
339 if __name__ == '__main__':
340 d = doodle3dConnect()
341 print 'Searching for Doodle3D box'
342 while not d.isAvailable():
345 while d.isPrinting():
346 print 'Doodle3D already printing! Requesting stop!'
350 print 'Doodle3D box found, printing!'
351 d.loadFile("C:/Models/belt-tensioner-wave_export.gcode")
353 while d.isPrinting() and d.isAvailable():
355 print d.getTemperature(0), d.getStatusString(), d.getPrintProgress(), d._progressLine, d._lineCount, d._blockIndex, len(d._fileBlocks)