From: daid Date: Mon, 23 Dec 2013 12:36:54 +0000 (+0100) Subject: Update the printerconnections for multiple Doodle3D box support. X-Git-Tag: 14.01~16 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=commitdiff_plain;h=ddbc824bdd59fc5ff45481de40c531d476f56bf8;p=cura.git Update the printerconnections for multiple Doodle3D box support. --- diff --git a/Cura/gui/printWindow2.py b/Cura/gui/printWindow2.py index 3ad0e4f9..8ce23b00 100644 --- a/Cura/gui/printWindow2.py +++ b/Cura/gui/printWindow2.py @@ -3,14 +3,11 @@ __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AG import wx import power import time -import os -import datetime from wx.lib import buttons from Cura.util import profile from Cura.util import resources -from Cura.gui.util import webcam class printWindow(wx.Frame): "Main user interface window" @@ -177,47 +174,8 @@ class printWindow(wx.Frame): self.UpdateButtonStates() - if webcam.hasWebcamSupport(): - #Need to call the camera class on the GUI thread, or else it won't work. Shame as it hangs the GUI for about 2 seconds. - wx.CallAfter(self._webcamCheck) - self._printerConnection.addCallback(self._doPrinterConnectionUpdate) - def _webcamCheck(self): - self.cam = webcam.webcam() - if self.cam.hasCamera(): - self.camPage = wx.Panel(self.tabs) - sizer = wx.GridBagSizer(2, 2) - self.camPage.SetSizer(sizer) - - self.timelapsEnable = wx.CheckBox(self.camPage, -1, _("Enable timelapse movie recording")) - self.timelapsSavePath = wx.TextCtrl(self.camPage, -1, os.path.expanduser('~/timelaps_' + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M') + '.mpg')) - sizer.Add(self.timelapsEnable, pos=(0, 0), span=(1, 2), flag=wx.EXPAND) - sizer.Add(self.timelapsSavePath, pos=(1, 0), span=(1, 2), flag=wx.EXPAND) - - pages = self.cam.propertyPages() - self.cam.buttons = [self.timelapsEnable, self.timelapsSavePath] - for page in pages: - button = wx.Button(self.camPage, -1, page) - button.index = pages.index(page) - sizer.Add(button, pos=(2, pages.index(page))) - button.Bind(wx.EVT_BUTTON, self.OnWebcamPropertyPageButton) - self.cam.buttons.append(button) - - self.campreviewEnable = wx.CheckBox(self.camPage, -1, _("Show preview")) - sizer.Add(self.campreviewEnable, pos=(3, 0), span=(1, 2), flag=wx.EXPAND) - - self.camPreview = wx.Panel(self.camPage) - sizer.Add(self.camPreview, pos=(4, 0), span=(1, 2), flag=wx.EXPAND) - - self.tabs.AddPage(self.camPage, _("Camera")) - self.camPreview.timer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.OnCameraTimer, self.camPreview.timer) - self.camPreview.timer.Start(500) - self.camPreview.Bind(wx.EVT_ERASE_BACKGROUND, self.OnCameraEraseBackground) - else: - self.cam = None - def OnPowerWarningChange(self, e): type = self.powerManagement.get_providing_power_source_type() if type == power.POWER_TYPE_AC and self.powerWarningText.IsShown(): @@ -331,31 +289,6 @@ class printWindow(wx.Frame): self.temperatureHeatUp.Enable(self._printerConnection.isAbleToSendDirectCommand()) self.termInput.Enable(self._printerConnection.isAbleToSendDirectCommand()) - def OnWebcamPropertyPageButton(self, e): - self.cam.openPropertyPage(e.GetEventObject().index) - - def OnCameraTimer(self, e): - if not self.campreviewEnable.GetValue(): - return - if self.machineCom is not None and self.machineCom.isPrinting(): - return - self.cam.takeNewImage() - self.camPreview.Refresh() - - def OnCameraEraseBackground(self, e): - dc = e.GetDC() - if not dc: - dc = wx.ClientDC(self) - rect = self.GetUpdateRegion().GetBox() - dc.SetClippingRect(rect) - dc.SetBackground(wx.Brush(self.camPreview.GetBackgroundColour(), wx.SOLID)) - if self.cam.getLastImage() is not None: - self.camPreview.SetMinSize((self.cam.getLastImage().GetWidth(), self.cam.getLastImage().GetHeight())) - self.camPage.Fit() - dc.DrawBitmap(self.cam.getLastImage(), 0, 0) - else: - dc.Clear() - class PrintCommandButton(buttons.GenBitmapButton): def __init__(self, parent, commandList, bitmapFilename, size=(20, 20)): self.bitmap = wx.Bitmap(resources.getPathForImage(bitmapFilename)) diff --git a/Cura/gui/sceneView.py b/Cura/gui/sceneView.py index 9e60a092..a3921c1d 100644 --- a/Cura/gui/sceneView.py +++ b/Cura/gui/sceneView.py @@ -227,10 +227,10 @@ class SceneView(openglGui.glGuiPanel): def OnPrintButton(self, button): if button == 1: - connectionEntry = self._printerConnectionManager.getAvailableConnection() + connectionGroup = self._printerConnectionManager.getAvailableGroup() if machineCom.machineIsConnected(): self.showPrintWindow() - elif len(removableStorage.getPossibleSDcardDrives()) > 0 and (connectionEntry is None or connectionEntry.priority < 0): + elif len(removableStorage.getPossibleSDcardDrives()) > 0 and (connectionGroup is None or connectionGroup.getPriority() < 0): drives = removableStorage.getPossibleSDcardDrives() if len(drives) > 1: dlg = wx.SingleChoiceDialog(self, "Select SD drive", "Multiple removable drives have been found,\nplease select your SD card drive", map(lambda n: n[0], drives)) @@ -243,27 +243,46 @@ class SceneView(openglGui.glGuiPanel): drive = drives[0] filename = self._scene._objectList[0].getName() + '.gcode' threading.Thread(target=self._copyFile,args=(self._gcodeFilename, drive[1] + filename, drive[1])).start() - elif connectionEntry is not None: - connection = connectionEntry.connection - if connectionEntry.window is None or not connectionEntry.window: - connectionEntry.window = printWindow2.printWindow(connection) - connectionEntry.window.Show() - connectionEntry.window.Raise() - if not connection.loadFile(self._gcodeFilename): - if connection.isPrinting(): - self.notification.message("Cannot start print, because other print still running.") - else: - self.notification.message("Failed to start print...") + elif connectionGroup is not None: + connections = connectionGroup.getAvailableConnections() + if len(connections) < 2: + connection = connections[0] + else: + dlg = wx.SingleChoiceDialog(self, "Select the %s connection to use" % (connectionGroup.getName()), "Multiple %s connections found" % (connectionGroup.getName()), map(lambda n: n.getName(), connections)) + if dlg.ShowModal() != wx.ID_OK: + dlg.Destroy() + return + connection = connections[dlg.GetSelection()] + dlg.Destroy() + self._openPrintWindowForConnection(connection) else: self.showSaveGCode() if button == 3: menu = wx.Menu() self.Bind(wx.EVT_MENU, lambda e: self.showPrintWindow(), menu.Append(-1, _("Print with USB"))) + connections = self._printerConnectionManager.getAvailableConnections() + menu.connectionMap = {} + for connection in connections: + i = menu.Append(-1, _("Print with %s") % (connection.getName())) + menu.connectionMap[i.GetId()] = connection + self.Bind(wx.EVT_MENU, lambda e: self._openPrintWindowForConnection(e.GetEventObject().connectionMap[e.GetId()]), i) self.Bind(wx.EVT_MENU, lambda e: self.showSaveGCode(), menu.Append(-1, _("Save GCode..."))) self.Bind(wx.EVT_MENU, lambda e: self._showSliceLog(), menu.Append(-1, _("Slice engine log..."))) self.PopupMenu(menu) menu.Destroy() + def _openPrintWindowForConnection(self, connection): + print '_openPrintWindowForConnection', connection.getName() + if connection.window is None or not connection.window: + connection.window = printWindow2.printWindow(connection) + connection.window.Show() + connection.window.Raise() + if not connection.loadFile(self._gcodeFilename): + if connection.isPrinting(): + self.notification.message("Cannot start print, because other print still running.") + else: + self.notification.message("Failed to start print...") + def showPrintWindow(self): if self._gcodeFilename is None: return @@ -907,16 +926,16 @@ class SceneView(openglGui.glGuiPanel): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def OnPaint(self,e): - connectionEntry = self._printerConnectionManager.getAvailableConnection() + connectionGroup = self._printerConnectionManager.getAvailableGroup() if machineCom.machineIsConnected(): self.printButton._imageID = 6 self.printButton._tooltip = _("Print") - elif len(removableStorage.getPossibleSDcardDrives()) > 0 and (connectionEntry is None or connectionEntry.priority < 0): + elif len(removableStorage.getPossibleSDcardDrives()) > 0 and (connectionGroup is None or connectionGroup.getPriority() < 0): self.printButton._imageID = 2 self.printButton._tooltip = _("Toolpath to SD") - elif connectionEntry is not None: - self.printButton._imageID = connectionEntry.icon - self.printButton._tooltip = _("Print with %s") % (connectionEntry.name) + elif connectionGroup is not None: + self.printButton._imageID = connectionGroup.getIconID() + self.printButton._tooltip = _("Print with %s") % (connectionGroup.getName()) else: self.printButton._imageID = 3 self.printButton._tooltip = _("Save toolpath") diff --git a/Cura/util/printerConnection/doodle3dConnect.py b/Cura/util/printerConnection/doodle3dConnect.py index 822a5042..bb03cd75 100644 --- a/Cura/util/printerConnection/doodle3dConnect.py +++ b/Cura/util/printerConnection/doodle3dConnect.py @@ -8,17 +8,100 @@ import time from Cura.util.printerConnection import printerConnectionBase -#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): +class doodle3dConnectionGroup(printerConnectionBase.printerConnectionGroup): PRINTER_LIST_HOST = 'connect.doodle3d.com' PRINTER_LIST_PATH = '/api/list.php' def __init__(self): - super(doodle3dConnect, self).__init__() + 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 getIconID(self): + return 27 + + def getPriority(self): + return 100 + + def __cmp__(self, other): + return self.getPriority() - other.getPriority() + + def __repr__(self): + return self.name + + 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, host, name, group): + super(doodle3dConnect, self).__init__(name) self._http = None - self._host = None + self._group = group + self._host = host + self._isAvailable = False self._printing = False self._fileBlocks = [] @@ -29,6 +112,7 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): self._hotendTemperature = [None] * 4 self._bedTemperature = None self._errorCount = 0 + self._interruptSleep = False self.checkThread = threading.Thread(target=self._doodle3DThread) self.checkThread.daemon = True @@ -72,6 +156,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): @@ -103,6 +188,7 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): 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. @@ -130,41 +216,12 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): return self._bedTemperature def _doodle3DThread(self): - waitDelay = 0 while True: - while self._host is None: - printerList = self._request('GET', self.PRINTER_LIST_PATH, host=self.PRINTER_LIST_HOST) - 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']: - status = self._request('GET', '/d3dapi/info/status', host=possiblePrinter['localip']) - if status and 'data' in status: - self._host = possiblePrinter['localip'] - break - - if self._host is None: - #If we cannot find a doodle3d box, delay a minute and request the list again. - # This so we do not stress the connect.doodle3d.com api too much - if waitDelay < 10: - waitDelay += 1 - time.sleep(waitDelay * 60) - else: - #If we found a doodle3D box, reset the wait delay, so we can find it again in case it gets lost - self._errorCount = 0 - waitDelay = 0 - stateReply = self._request('GET', '/d3dapi/info/status') if stateReply is None or not stateReply: - # No API, wait 15 seconds before looking for Doodle3D again. + # 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: self._host = None @@ -172,15 +229,18 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): self._printing = False self._isAvailable = False self._doCallback() - time.sleep(15) + self._sleep(15) + else: + self._sleep(3) continue if stateReply['data']['state'] == 'disconnected': - # No printer connected + # 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() - time.sleep(5) + self._sleep(15) continue self._errorCount = 0 @@ -199,7 +259,7 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): if self._request('POST', '/d3dapi/printer/print', {'gcode': self._fileBlocks[self._blockIndex], 'start': 'True', 'first': 'True'}): self._blockIndex += 1 else: - time.sleep(1) + self._sleep(1) else: self._printing = False else: @@ -207,9 +267,9 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): if self._request('POST', '/d3dapi/printer/print', {'gcode': self._commandList[0], 'start': 'True', 'first': 'True'}): self._commandList.pop(0) else: - time.sleep(1) + self._sleep(1) else: - time.sleep(5) + self._sleep(5) elif stateReply['data']['state'] == 'printing': if self._printing: if self._blockIndex < len(self._fileBlocks): @@ -219,11 +279,11 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): self._blockIndex += 1 else: #Cannot send new block, wait a bit, so we do not overload the API - time.sleep(15) + 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) + self._sleep(5) if 'current_line' in stateReply['data']: self._progressLine = stateReply['data']['current_line'] else: @@ -234,9 +294,16 @@ class doodle3dConnect(printerConnectionBase.printerConnectionBase): self._blockIndex = 1 self._progressLine = stateReply['data']['current_line'] self._lineCount = stateReply['data']['total_lines'] - time.sleep(5) + 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 diff --git a/Cura/util/printerConnection/dummyConnection.py b/Cura/util/printerConnection/dummyConnection.py index ae2d5e88..4d8dee0f 100644 --- a/Cura/util/printerConnection/dummyConnection.py +++ b/Cura/util/printerConnection/dummyConnection.py @@ -8,10 +8,18 @@ import time from Cura.util.printerConnection import printerConnectionBase +class dummyConnectionGroup(printerConnectionBase.printerConnectionGroup): + def __init__(self): + super(dummyConnectionGroup, self).__init__("Dummy") + self._list = [dummyConnection("Dummy 1"), dummyConnection("Dummy 2")] + + def getAvailableConnections(self): + return self._list + #Dummy printer class which is always class dummyConnection(printerConnectionBase.printerConnectionBase): - def __init__(self): - super(dummyConnection, self).__init__() + def __init__(self, name): + super(dummyConnection, self).__init__(name) self._printing = False self._lineCount = 0 diff --git a/Cura/util/printerConnection/printerConnectionBase.py b/Cura/util/printerConnection/printerConnectionBase.py index c886f6e7..41594fef 100644 --- a/Cura/util/printerConnection/printerConnectionBase.py +++ b/Cura/util/printerConnection/printerConnectionBase.py @@ -2,6 +2,28 @@ __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AG import traceback +class printerConnectionGroup(object): + def __init__(self, name): + self._name = name + + def getAvailableConnections(self): + return [] + + def getName(self): + return self._name + + def getIconID(self): + return 5 + + def getPriority(self): + return -1 + + def __cmp__(self, other): + return self.getPriority() - other.getPriority() + + def __repr__(self): + return self.name + #Base class for different printer connection implementations. # A printer connection can connect to printers in different ways, trough network, USB or carrier pigeons. # Each printer connection has different capabilities that you can query with the "has" functions. @@ -9,8 +31,13 @@ import traceback # Each printer connection has callback objects that receive status updates from the printer when information changes. class printerConnectionBase(object): - def __init__(self): + def __init__(self, name): self._callbackList = [] + self._name = name + self.window = None + + def getName(self): + return self._name #Load the file into memory for printing, returns True on success def loadFile(self, filename): diff --git a/Cura/util/printerConnection/printerConnectionManager.py b/Cura/util/printerConnection/printerConnectionManager.py index c3776275..674b86ae 100644 --- a/Cura/util/printerConnection/printerConnectionManager.py +++ b/Cura/util/printerConnection/printerConnectionManager.py @@ -4,33 +4,26 @@ from Cura.util import version from Cura.util.printerConnection import dummyConnection from Cura.util.printerConnection import doodle3dConnect -class connectionEntry(object): - def __init__(self, name, priority, icon, connection): - self.name = name - self.priority = priority - self.icon = icon - self.connection = connection - self.window = None - - def __cmp__(self, other): - return self.priority - other.priority - - def __repr__(self): - return self.name - class PrinterConnectionManager(object): def __init__(self): - self._connectionList = [] + self._groupList = [] if version.isDevVersion(): - self._connectionList.append(connectionEntry('Dummy', -1, 5, dummyConnection.dummyConnection())) - self._connectionList.append(connectionEntry('Doodle3D', 100, 27, doodle3dConnect.doodle3dConnect())) + self._groupList.append(dummyConnection.dummyConnectionGroup()) + #self._groupList.append(doodle3dConnect.doodle3dConnectionGroup()) #Sort the connections by highest priority first. - self._connectionList.sort(reverse=True) + self._groupList.sort(reverse=True) #Return the highest priority available connection. - def getAvailableConnection(self): - for e in self._connectionList: - if e.connection.isAvailable(): - return e + def getAvailableGroup(self): + for g in self._groupList: + if len(g.getAvailableConnections()) > 0: + return g return None + + #Return all available connections. + def getAvailableConnections(self): + ret = [] + for e in self._groupList: + ret += e.getAvailableConnections() + return ret