chiark / gitweb /
Add version 0.1 of serial communication with printerConnection.
[cura.git] / Cura / util / printerConnection / doodle3dConnect.py
index 7abc4631b7c633537ef360dc9e67f1d695a29ded..ad3ea51b27883800b6f2bcc45b2c2329262044af 100644 (file)
@@ -8,41 +8,122 @@ import time
 
 from Cura.util.printerConnection import printerConnectionBase
 
-#Always LAN IP: 192.168.5.1
-#Request: http://connect.doodle3d.com/api/list.php
-#      {"status":"success","data":[{"id":"62.216.8.197\/10.0.0.35","0":"62.216.8.197\/10.0.0.35","remoteip":"62.216.8.197","1":"62.216.8.197","localip":"10.0.0.35","2":"10.0.0.35","wifiboxid":"Doodle3D-87354C","3":"Doodle3D-87354C","hidden":"0","4":"0","date":"2013-11-26 22:02:45","5":"2013-11-26 22:02:45"}],"debug":{"time":1385499835,"hourago":1385496235,"remoteip":"62.216.8.197"}}
-#TODO: api/info/status
+class doodle3dConnectionGroup(printerConnectionBase.printerConnectionGroup):
+       PRINTER_LIST_HOST = 'connect.doodle3d.com'
+       PRINTER_LIST_PATH = '/api/list.php'
+
+       def __init__(self):
+               super(doodle3dConnectionGroup, self).__init__("Doodle3D")
+               self._http = None
+               self._host = self.PRINTER_LIST_HOST
+               self._connectionMap = {}
+
+               self._thread = threading.Thread(target=self._doodle3DThread)
+               self._thread.daemon = True
+               self._thread.start()
+
+       def getAvailableConnections(self):
+               return filter(lambda c: c.isAvailable(), self._connectionMap.values())
+
+       def remove(self, host):
+               del self._connectionMap[host]
+
+       def getIconID(self):
+               return 27
+
+       def getPriority(self):
+               return 100
+
+       def _doodle3DThread(self):
+               self._waitDelay = 0
+               while True:
+                       printerList = self._request('GET', self.PRINTER_LIST_PATH)
+                       if not printerList or type(printerList) is not dict or 'data' not in printerList or type(printerList['data']) is not list:
+                               #Check if we are connected to the Doodle3D box in access point mode, as this gives an
+                               # invalid reply on the printer list API
+                               printerList = {'data': [{'localip': 'draw.doodle3d.com'}]}
+
+                       #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.
+                       # (connect.doodle3d.com also checks for this IP in the javascript code)
+                       printerList['data'].append({'localip': '192.168.5.1'})
+
+                       #Check the status of each possible IP, if we find a valid box with a printer connected. Use that IP.
+                       for possiblePrinter in printerList['data']:
+                               if possiblePrinter['localip'] not in self._connectionMap:
+                                       status = self._request('GET', '/d3dapi/config/?network.cl.wifiboxid=', host=possiblePrinter['localip'])
+                                       if status and 'data' in status and 'network.cl.wifiboxid' in status['data']:
+                                               self._connectionMap[possiblePrinter['localip']] = doodle3dConnect(possiblePrinter['localip'], status['data']['network.cl.wifiboxid'], self)
+
+                       # Delay a bit more after every request. This so we do not stress the connect.doodle3d.com api too much
+                       if self._waitDelay < 10:
+                               self._waitDelay += 1
+                       time.sleep(self._waitDelay * 60)
+
+       def _request(self, method, path, postData = None, host = None):
+               if host is None:
+                       host = self._host
+               if self._http is None or self._http.host != host:
+                       self._http = httpclient.HTTPConnection(host, timeout=30)
+
+               try:
+                       if postData is not None:
+                               self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
+                       else:
+                               self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
+               except:
+                       self._http.close()
+                       return None
+               try:
+                       response = self._http.getresponse()
+                       responseText = response.read()
+               except:
+                       self._http.close()
+                       return None
+               try:
+                       response = json.loads(responseText)
+               except ValueError:
+                       self._http.close()
+                       return None
+               if response['status'] != 'success':
+                       return False
+
+               return response
 
 #Class to connect and print files with the doodle3d.com wifi box
 # Auto-detects if the Doodle3D box is available with a printer
 class doodle3dConnect(printerConnectionBase.printerConnectionBase):
-       def __init__(self):
-               super(doodle3dConnect, self).__init__()
+       def __init__(self, host, name, group):
+               super(doodle3dConnect, self).__init__(name)
 
                self._http = None
+               self._group = group
+               self._host = host
+
                self._isAvailable = False
                self._printing = False
                self._fileBlocks = []
+               self._commandList = []
                self._blockIndex = None
                self._lineCount = 0
                self._progressLine = 0
                self._hotendTemperature = [None] * 4
                self._bedTemperature = None
+               self._errorCount = 0
+               self._interruptSleep = False
 
-               self.checkThread = threading.Thread(target=self._doodle3Dthread)
+               self.checkThread = threading.Thread(target=self._doodle3DThread)
                self.checkThread.daemon = True
                self.checkThread.start()
 
        #Load the file into memory for printing.
-       def loadFile(self, filename):
+       def loadGCodeData(self, dataStream):
                if self._printing:
-                       return
+                       return False
                self._fileBlocks = []
                self._lineCount = 0
                block = []
                blockSize = 0
-               f = open(filename, "r")
-               for line in f:
+               for line in dataStream:
                        #Strip out comments, we do not need to send comments
                        if ';' in line:
                                line = line[:line.index(';')]
@@ -52,15 +133,16 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase):
                        if len(line) < 1:
                                continue
                        self._lineCount += 1
-                       #Put the lines in 2k sized blocks, so we can send those blocks as http requests.
-                       if blockSize + len(line) > 2048:
+                       #Put the lines in 8k sized blocks, so we can send those blocks as http requests.
+                       if blockSize + len(line) > 1024 * 8:
                                self._fileBlocks.append('\n'.join(block) + '\n')
                                block = []
                                blockSize = 0
                        blockSize += len(line) + 1
                        block.append(line)
                self._fileBlocks.append('\n'.join(block) + '\n')
-               f.close()
+               self._doCallback()
+               return True
 
        #Start printing the previously loaded file
        def startPrint(self):
@@ -69,6 +151,7 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase):
                self._progressLine = 0
                self._blockIndex = 0
                self._printing = True
+               self._interruptSleep = True
 
        #Abort the previously loaded print file
        def cancelPrint(self):
@@ -90,12 +173,34 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase):
        def isAvailable(self):
                return self._isAvailable
 
+       #Are we able to send a direct coammand with sendCommand at this moment in time.
+       def isAbleToSendDirectCommand(self):
+               #The delay on direct commands is very high and so we disabled it.
+               return False #self._isAvailable and not self._printing
+
+       #Directly send a command to the printer.
+       def sendCommand(self, command):
+               if not self._isAvailable or self._printing:
+                       return
+               self._commandList.append(command)
+               self._interruptSleep = True
+
        # Get the connection status string. This is displayed to the user and can be used to communicate
        #  various information to the user.
        def getStatusString(self):
                if not self._isAvailable:
                        return "Doodle3D box not found"
-               return "TODO"
+               if self._printing:
+                       ret = "Print progress: %.1f%%" % (self.getPrintProgress() * 100.0)
+                       if self._blockIndex < len(self._fileBlocks):
+                               ret += "\nSending GCode: %.1f%%" % (float(self._blockIndex) * 100.0 / float(len(self._fileBlocks)))
+                       elif len(self._fileBlocks) > 0:
+                               ret += "\nFinished sending GCode to Doodle3D box.\nPrint will continue even if you shut down Cura."
+                       else:
+                               ret += "\nDifferent print still running..."
+                       ret += "\nErrorCount: %d" % (self._errorCount)
+                       return ret
+               return "Printer found, waiting for print command."
 
        #Get the temperature of an extruder, returns None is no temperature is known for this extruder
        def getTemperature(self, extruder):
@@ -105,32 +210,62 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase):
        def getBedTemperature(self):
                return self._bedTemperature
 
-       def _doodle3Dthread(self):
+       def _doodle3DThread(self):
                while True:
-                       stateReply = self._request('GET', '/d3dapi/printer/state')
-                       if stateReply is None:  #No API, wait 15 seconds before looking for Doodle3D again.
-                               self._isAvailable = False
-                               self._doCallback()
-                               time.sleep(15)
+                       stateReply = self._request('GET', '/d3dapi/info/status')
+                       if stateReply is None or not stateReply:
+                               # No API, wait 5 seconds before looking for Doodle3D again.
+                               # API gave back an error (this can happen if the Doodle3D box is connecting to the printer)
+                               # The Doodle3D box could also be offline, if we reach a high enough errorCount then assume the box is gone.
+                               self._errorCount += 1
+                               if self._errorCount > 10:
+                                       if self._isAvailable:
+                                               self._printing = False
+                                               self._isAvailable = False
+                                               self._doCallback()
+                                       self._sleep(15)
+                                       self._group.remove(self._host)
+                                       return
+                               else:
+                                       self._sleep(3)
                                continue
-                       if not stateReply:              #API gave back an error (this can happen if the Doodle3D box is connecting to the printer)
-                               self._isAvailable = False
-                               self._doCallback()
-                               time.sleep(5)
+                       if stateReply['data']['state'] == 'disconnected':
+                               # No printer connected, we do not have a printer available, but the Doodle3D box is there.
+                               # So keep trying to find a printer connected to it.
+                               if self._isAvailable:
+                                       self._printing = False
+                                       self._isAvailable = False
+                                       self._doCallback()
+                               self._sleep(15)
                                continue
+                       self._errorCount = 0
+
+                       #We got a valid status, set the doodle3d printer as available.
                        if not self._isAvailable:
                                self._isAvailable = True
-                               self._doCallback()
 
-                       if stateReply['data']['state'] == 'idle':
+                       if 'hotend' in stateReply['data']:
+                               self._hotendTemperature[0] = stateReply['data']['hotend']
+                       if 'bed' in stateReply['data']:
+                               self._bedTemperature = stateReply['data']['bed']
+
+                       if stateReply['data']['state'] == 'idle' or stateReply['data']['state'] == 'buffering':
                                if self._printing:
                                        if self._blockIndex < len(self._fileBlocks):
                                                if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex], 'start': 'True', 'first': 'True'}):
                                                        self._blockIndex += 1
+                                               else:
+                                                       self._sleep(1)
                                        else:
                                                self._printing = False
                                else:
-                                       time.sleep(5)
+                                       if len(self._commandList) > 0:
+                                               if self._request('POST', '/d3dapi/printer/print', {'gcode': self._commandList[0], 'start': 'True', 'first': 'True'}):
+                                                       self._commandList.pop(0)
+                                               else:
+                                                       self._sleep(1)
+                                       else:
+                                               self._sleep(5)
                        elif stateReply['data']['state'] == 'printing':
                                if self._printing:
                                        if self._blockIndex < len(self._fileBlocks):
@@ -138,33 +273,44 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase):
                                                        if self._blockIndex < len(self._fileBlocks):
                                                                if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex]}):
                                                                        self._blockIndex += 1
+                                                               else:
+                                                                       #Cannot send new block, wait a bit, so we do not overload the API
+                                                                       self._sleep(15)
+                                                                       break
                                        else:
                                                #If we are no longer sending new GCode delay a bit so we request the status less often.
-                                               time.sleep(5)
-                                       progress = self._request('GET', '/d3dapi/printer/progress')
-                                       if progress:
-                                               self._progressLine = progress['data']['current_line']
-                                       temperature = self._request('GET', '/d3dapi/printer/temperature')
-                                       if temperature:
-                                               self._hotendTemperature[0] = temperature['data']['hotend']
-                                               self._bedTemperature = temperature['data']['bed']
+                                               self._sleep(5)
+                                       if 'current_line' in stateReply['data']:
+                                               self._progressLine = stateReply['data']['current_line']
                                else:
                                        #Got a printing state without us having send the print file, set the state to printing, but make sure we never send anything.
-                                       progress = self._request('GET', '/d3dapi/printer/progress')
-                                       if progress:
+                                       if 'current_line' in stateReply['data'] and 'total_lines' in stateReply['data'] and stateReply['data']['total_lines'] > 2:
                                                self._printing = True
-                                               self._blockIndex = len(self._fileBlocks)
-                                               self._lineCount = progress['data']['total_lines']
-                                       time.sleep(5)
+                                               self._fileBlocks = []
+                                               self._blockIndex = 1
+                                               self._progressLine = stateReply['data']['current_line']
+                                               self._lineCount = stateReply['data']['total_lines']
+                                       self._sleep(5)
+                       self._doCallback()
+
+       def _sleep(self, timeOut):
+               while timeOut > 0.0:
+                       if not self._interruptSleep:
+                               time.sleep(0.1)
+                       timeOut -= 0.1
+               self._interruptSleep = False
+
+       def _request(self, method, path, postData = None, host = None):
+               if host is None:
+                       host = self._host
+               if self._http is None or self._http.host != host:
+                       self._http = httpclient.HTTPConnection(host, timeout=30)
 
-       def _request(self, method, path, postData = None):
-               if self._http is None:
-                       self._http = httpclient.HTTPConnection('draw.doodle3d.com')
                try:
                        if postData is not None:
-                               self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded"})
+                               self._http.request(method, path, urllib.urlencode(postData), {"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
                        else:
-                               self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded"})
+                               self._http.request(method, path, headers={"Content-type": "application/x-www-form-urlencoded", "User-Agent": "Cura Doodle3D connection"})
                except:
                        self._http.close()
                        return None