diff --git a/DiscoverUM3Action.py b/DiscoverUM3Action.py index 6c6757d28b..3d69a8c118 100644 --- a/DiscoverUM3Action.py +++ b/DiscoverUM3Action.py @@ -52,6 +52,22 @@ class DiscoverUM3Action(MachineAction): else: self._network_plugin.startDiscovery() + @pyqtSlot(str, str) + def removeManualPrinter(self, key, address): + if not self._network_plugin: + return + + self._network_plugin.removeManualPrinter(key, address) + + @pyqtSlot(str, str) + def setManualPrinter(self, key, address): + if key != "": + # This manual printer replaces a current manual printer + self._network_plugin.removeManualPrinter(key) + + if address != "": + self._network_plugin.addManualPrinter(address) + def _onPrinterDiscoveryChanged(self, *args): self._last_zeroconf_event_time = time.time() self.printersChanged.emit() diff --git a/DiscoverUM3Action.qml b/DiscoverUM3Action.qml index 8b20066ab2..159d72a821 100644 --- a/DiscoverUM3Action.qml +++ b/DiscoverUM3Action.qml @@ -11,6 +11,7 @@ Cura.MachineAction id: base anchors.fill: parent; property var selectedPrinter: null + property bool completeProperties: true property var connectingToPrinter: null Connections @@ -66,13 +67,45 @@ Cura.MachineAction text: catalog.i18nc("@label", "To print directly to your Ultimaker 3 printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your Ultimaker 3, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your Ultimaker 3 from the list below:") } - Button + Row { - id: rediscoverButton - text: catalog.i18nc("@title", "Refresh") - onClicked: manager.restartDiscovery() - anchors.right: parent.right - anchors.rightMargin: parent.width * 0.5 + spacing: UM.Theme.getSize("default_lining").width + + Button + { + id: addButton + text: catalog.i18nc("@action:button", "Add"); + onClicked: + { + manualPrinterDialog.showDialog("", ""); + } + } + + Button + { + id: editButton + text: catalog.i18nc("@action:button", "Edit") + enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true" + onClicked: + { + manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress); + } + } + + Button + { + id: removeButton + text: catalog.i18nc("@action:button", "Remove") + enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true" + onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress) + } + + Button + { + id: rediscoverButton + text: catalog.i18nc("@title", "Refresh") + onClicked: manager.restartDiscovery() + } } Row @@ -118,7 +151,12 @@ Cura.MachineAction } width: parent.width currentIndex: -1 - onCurrentIndexChanged: base.selectedPrinter = listview.model[currentIndex] + onCurrentIndexChanged: + { + base.selectedPrinter = listview.model[currentIndex]; + // Only allow connecting if the printer has responded to API query since the last refresh + base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true"; + } Component.onCompleted: manager.startDiscovery() delegate: Rectangle { @@ -220,10 +258,68 @@ Cura.MachineAction Button { text: catalog.i18nc("@action:button", "Connect") - enabled: base.selectedPrinter ? true : false + enabled: (base.selectedPrinter && base.completeProperties) ? true : false onClicked: connectToPrinter() } } } } + + UM.Dialog + { + id: manualPrinterDialog + property string printerKey + property alias addressText: addressField.text + + title: catalog.i18nc("@label", "IP Address") + + minimumWidth: 400 * Screen.devicePixelRatio + minimumHeight: 120 * Screen.devicePixelRatio + width: minimumWidth + height: minimumHeight + + signal showDialog(string key, string address) + onShowDialog: + { + printerKey = key; + + addressText = address; + addressField.selectAll(); + addressField.focus = true; + + manualPrinterDialog.show(); + } + + onAccepted: + { + manager.setManualPrinter(printerKey, addressText) + } + + Column { + anchors.fill: parent + + TextField { + id: addressField + width: parent.width + maximumLength: 40 + validator: RegExpValidator + { + regExp: /[a-zA-Z0-9\.\-\_]*/ + } + } + } + + rightButtons: [ + Button { + text: catalog.i18nc("@action:button","Cancel") + onClicked: manualPrinterDialog.reject() + }, + Button { + text: catalog.i18nc("@action:button", "Ok") + onClicked: manualPrinterDialog.accept() + enabled: manualPrinterDialog.addressText.trim() != "" + isDefault: true + } + ] + } } \ No newline at end of file diff --git a/NetworkPrinterOutputDevice.py b/NetworkPrinterOutputDevice.py index e2fe36f118..e3eb15f859 100644 --- a/NetworkPrinterOutputDevice.py +++ b/NetworkPrinterOutputDevice.py @@ -36,11 +36,12 @@ class AuthState(IntEnum): ## Network connected (wifi / lan) printer that uses the Ultimaker API @signalemitter class NetworkPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, key, address, properties): + def __init__(self, key, address, properties, api_prefix): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf + self._api_prefix = api_prefix self._gcode = None self._print_finished = True # _print_finsihed == False means we're halfway in a print @@ -94,8 +95,6 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._material_ids = [""] * self._num_extruders self._hotend_ids = [""] * self._num_extruders - self._api_version = "1" - self._api_prefix = "/api/v" + self._api_version + "/" self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print over network")) @@ -187,6 +186,14 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): def getProperties(self): return self._properties + @pyqtSlot(str, result = str) + def getProperty(self, key): + key = key.encode("utf-8") + if key in self._properties: + return self._properties.get(key, b"").decode("utf-8") + else: + return "" + ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result = str) diff --git a/NetworkPrinterOutputDevicePlugin.py b/NetworkPrinterOutputDevicePlugin.py index f0966c9fe3..bb1fade0bc 100644 --- a/NetworkPrinterOutputDevicePlugin.py +++ b/NetworkPrinterOutputDevicePlugin.py @@ -5,8 +5,13 @@ from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from UM.Logger import Logger from UM.Signal import Signal, signalemitter from UM.Application import Application +from UM.Preferences import Preferences + +from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, QNetworkReply +from PyQt5.QtCore import QUrl import time +import json ## This plugin handles the connection detection & creation of output device objects for the UM3 printer. # Zero-Conf is used to detect printers, which are saved in a dict. @@ -19,6 +24,12 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._browser = None self._printers = {} + self._api_version = "1" + self._api_prefix = "/api/v" + self._api_version + "/" + + self._network_manager = QNetworkAccessManager() + self._network_manager.finished.connect(self._onNetworkRequestFinished) + # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces # authentication requests. self._old_printers = [] @@ -28,6 +39,11 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self.removePrinterSignal.connect(self.removePrinter) Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) + # Get list of manual printers from preferences + self._preferences = Preferences.getInstance() + self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames + self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",") + addPrinterSignal = Signal() removePrinterSignal = Signal() printerListChanged = Signal() @@ -49,6 +65,62 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): self._zero_conf = Zeroconf() self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged]) + # Look for manual instances from preference + for address in self._manual_instances: + if address: + self.addManualPrinter(address) + + def addManualPrinter(self, address): + if address not in self._manual_instances: + self._manual_instances.append(address) + self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) + + name = address + instance_name = "manual:%s" % address + properties = { b"name": name.encode("utf-8"), b"manual": b"true", b"incomplete": b"true" } + + if instance_name not in self._printers: + # Add a preliminary printer instance + self.addPrinter(instance_name, address, properties) + + self.checkManualPrinter(address) + + def removeManualPrinter(self, key, address = None): + if key in self._printers: + if not address: + address = self._printers[key].ipAddress + self.removePrinter(key) + + if address in self._manual_instances: + self._manual_instances.remove(address) + self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) + + def checkManualPrinter(self, address): + # Check if a printer exists at this address + # If a printer responds, it will replace the preliminary printer created above + url = QUrl("http://" + address + self._api_prefix + "system") + name_request = QNetworkRequest(url) + self._network_manager.get(name_request) + + ## Handler for all requests that have finished. + def _onNetworkRequestFinished(self, reply): + reply_url = reply.url().toString() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if reply.operation() == QNetworkAccessManager.GetOperation: + if "system" in reply_url: # Name returned from printer. + if status_code == 200: + system_info = json.loads(bytes(reply.readAll()).decode("utf-8")) + address = reply.url().host() + name = ("%s (%s)" % (system_info["name"], address)) + + instance_name = "manual:%s" % address + properties = { b"name": name.encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"), b"manual": b"true" } + if instance_name in self._printers: + # Only replace the printer if it is still in the list of (manual) printers + self.removePrinter(instance_name) + self.addPrinter(instance_name, address, properties) + ## Stop looking for devices on network. def stop(self): if self._zero_conf is not None: @@ -72,7 +144,7 @@ class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin): ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addPrinter(self, name, address, properties): - printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties) + printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix) self._printers[printer.getKey()] = printer global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):