diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 3585aee5ea..b10700176e 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -266,6 +266,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): else: return "" + def getProperties(self): + return self._properties + ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtProperty(str, constant=True) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index 84115f28d3..0e872fed43 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -12,7 +12,10 @@ from cura.MachineAction import MachineAction catalog = i18nCatalog("cura") + class DiscoverUM3Action(MachineAction): + discoveredDevicesChanged = pyqtSignal() + def __init__(self): super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) self._qml_url = "DiscoverUM3Action.qml" @@ -30,8 +33,6 @@ class DiscoverUM3Action(MachineAction): # Time to wait after a zero-conf service change before allowing a zeroconf reset self._zero_conf_change_grace_period = 0.25 - discoveredDevicesChanged = pyqtSignal() - @pyqtSlot() def startDiscovery(self): if not self._network_plugin: @@ -73,7 +74,7 @@ class DiscoverUM3Action(MachineAction): self._network_plugin.removeManualDevice(key) if address != "": - self._network_plugin.addManualPrinter(address) + self._network_plugin.addManualDevice(address) def _onDeviceDiscoveryChanged(self, *args): self._last_zero_conf_event_time = time.time() diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index d79bd543e7..003fdbf95c 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -101,7 +101,7 @@ Cura.MachineAction id: removeButton text: catalog.i18nc("@action:button", "Remove") enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true" - onClicked: manager.removeManualPrinter(base.selectedDevice.key, base.selectedDevice.ipAddress) + onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress) } Button @@ -343,7 +343,7 @@ Cura.MachineAction onAccepted: { - manager.setManualPrinter(printerKey, addressText) + manager.setManualDevice(printerKey, addressText) } Column { diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 13ab774577..fa1a0bc417 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -5,14 +5,21 @@ from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.Logger import Logger from UM.Application import Application from UM.Signal import Signal, signalemitter +from UM.Preferences import Preferences +from UM.Version import Version + +from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice + +from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager +from PyQt5.QtCore import QUrl from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from queue import Queue from threading import Event, Thread - from time import time -from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice +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. @@ -35,6 +42,23 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) self._discovered_devices = {} + + self._network_manager = QNetworkAccessManager() + self._network_manager.finished.connect(self._onNetworkRequestFinished) + + self._min_cluster_version = Version("4.0.0") + + self._api_version = "1" + self._api_prefix = "/api/v" + self._api_version + "/" + self._cluster_api_version = "1" + self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" + + # Get list of manual instances 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(",") # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests # which fail to get detailed service info. @@ -62,6 +86,11 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest]) + # Look for manual instances from preference + for address in self._manual_instances: + if address: + self.addManualDevice(address) + def reCheckConnections(self): active_machine = Application.getInstance().getGlobalContainerStack() if not active_machine: @@ -94,6 +123,94 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): Logger.log("d", "zeroconf close...") self._zero_conf.close() + def addManualDevice(self, address): + if address not in self._manual_instances: + self._manual_instances.append(address) + self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances)) + + instance_name = "manual:%s" % address + properties = { + b"name": address.encode("utf-8"), + b"address": address.encode("utf-8"), + b"manual": b"true", + b"incomplete": b"true" + } + + if instance_name not in self._discovered_devices: + # Add a preliminary printer instance + self._onAddDevice(instance_name, address, properties) + + self._checkManualDevice(address) + + def _checkManualDevice(self, address): + # Check if a UM3 family device exists at this address. + # If a printer responds, it will replace the preliminary printer created above + # origin=manual is for tracking back the origin of the call + url = QUrl("http://" + address + self._api_prefix + "system") + name_request = QNetworkRequest(url) + self._network_manager.get(name_request) + + def _onNetworkRequestFinished(self, reply): + reply_url = reply.url().toString() + + if "system" in reply_url: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + # Something went wrong with checking the firmware version! + return + + try: + system_info = json.loads(bytes(reply.readAll()).decode("utf-8")) + except: + Logger.log("e", "Something went wrong converting the JSON.") + return + + address = reply.url().host() + has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version + instance_name = "manual:%s" % address + properties = { + b"name": system_info["name"].encode("utf-8"), + b"address": address.encode("utf-8"), + b"firmware_version": system_info["firmware"].encode("utf-8"), + b"manual": b"true", + b"machine": system_info["variant"].encode("utf-8") + } + + if has_cluster_capable_firmware: + # Cluster needs an additional request, before it's completed. + properties[b"incomplete"] = b"true" + + # Check if the device is still in the list & re-add it with the updated + # information. + if instance_name in self._discovered_devices: + self._onRemoveDevice(instance_name) + self._onAddDevice(instance_name, address, properties) + + if has_cluster_capable_firmware: + # We need to request more info in order to figure out the size of the cluster. + cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/") + cluster_request = QNetworkRequest(cluster_url) + self._network_manager.get(cluster_request) + + elif "printers" in reply_url: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + # Something went wrong with checking the amount of printers the cluster has! + return + # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is. + try: + cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8")) + except: + Logger.log("e", "Something went wrong converting the JSON.") + return + address = reply.url().host() + instance_name = "manual:%s" % address + if instance_name in self._discovered_devices: + device = self._discovered_devices[instance_name] + properties = device.getProperties().copy() + del properties[b"incomplete"] + properties[b'cluster_size'] = len(cluster_printers_list) + self._onRemoveDevice(instance_name) + self._onAddDevice(instance_name, address, properties) + def _onRemoveDevice(self, device_id): device = self._discovered_devices.pop(device_id, None) if device: