diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 15d901ba67..4f89513e1e 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -58,6 +58,14 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # Therefore we create a private signal used to trigger the printersChanged signal. _clusterPrintersChanged = pyqtSignal() + # Map of Cura Connect machine_variant field to Cura machine types. + # Needed for printer discovery stack creation. + _host_machine_variant_to_machine_type_map = { + "Ultimaker 3": "ultimaker3", + "Ultimaker 3 Extended": "ultimaker3_extended", + "Ultimaker S5": "ultimaker_s5" + } + ## Creates a new cloud output device # \param api_client: The client that will run the API calls # \param cluster: The device response received from the cloud API. @@ -68,10 +76,10 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # Because the cloud connection does not off all of these, we manually construct this version here. # An example of why this is needed is the selection of the compatible file type when exporting the tool path. properties = { - b"address": b"", - b"name": cluster.host_name.encode() if cluster.host_name else b"", + b"address": cluster.host_internal_ip.encode() if cluster.host_internal_ip else b"", + b"name": cluster.friendly_name.encode() if cluster.friendly_name else b"", b"firmware_version": cluster.host_version.encode() if cluster.host_version else b"", - b"printer_type": b"" + b"cluster_size": b"1" # cloud devices are always clusters of at least one } super().__init__(device_id = cluster.cluster_id, address = "", @@ -96,6 +104,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # We keep track of which printer is visible in the monitor page. self._active_printer = None # type: Optional[PrinterOutputModel] + self._host_machine_type = "" # Properties to populate later on with received cloud data. self._print_jobs = [] # type: List[UM3PrintJobOutputModel] @@ -236,6 +245,10 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel] received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinterStatus] + if len(printers) > 0: + # We need the machine type of the host (1st list entry) to allow discovery to work. + self._host_machine_type = printers[0].machine_variant + removed_printers, added_printers, updated_printers = findChanges(previous, received) for removed_printer in removed_printers: @@ -359,6 +372,19 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): ).show() self.writeFinished.emit() + ## Gets the printer type of the cluster host. Falls back to the printer type in the device properties. + @pyqtProperty(str, notify=_clusterPrintersChanged) + def printerType(self) -> str: + if self._printers and self._host_machine_type in self._host_machine_variant_to_machine_type_map: + return self._host_machine_variant_to_machine_type_map[self._host_machine_type] + return super().printerType + + ## Gets the number of printers in the cluster. + # We use a minimum of 1 because cloud devices are always a cluster and printer discovery needs it. + @pyqtProperty(int, notify = _clusterPrintersChanged) + def clusterSize(self) -> int: + return max(1, len(self._printers)) + ## Gets the remote printers. @pyqtProperty("QVariantList", notify=_clusterPrintersChanged) def printers(self) -> List[PrinterOutputModel]: @@ -376,10 +402,6 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): self._active_printer = printer self.activePrinterChanged.emit() - @pyqtProperty(int, notify = _clusterPrintersChanged) - def clusterSize(self) -> int: - return len(self._printers) - ## Get remote print jobs. @pyqtProperty("QVariantList", notify = printJobsChanged) def printJobs(self) -> List[UM3PrintJobOutputModel]: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 67245eb357..498e141b73 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -7,7 +7,7 @@ from PyQt5.QtCore import QTimer from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message -from UM.Signal import Signal, signalemitter +from UM.Signal import Signal from cura.API import Account from cura.CuraApplication import CuraApplication from cura.Settings.GlobalStack import GlobalStack @@ -81,25 +81,61 @@ class CloudOutputDeviceManager: Logger.log("d", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates)) # Remove output devices that are gone - for removed_cluster in removed_devices: - if removed_cluster.isConnected(): - removed_cluster.disconnect() - removed_cluster.close() - self._output_device_manager.removeOutputDevice(removed_cluster.key) - self.removedCloudCluster.emit(removed_cluster) - del self._remote_clusters[removed_cluster.key] + for device in removed_devices: + if device.isConnected(): + device.disconnect() + device.close() + self._output_device_manager.removeOutputDevice(device.key) + self._application.getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key) + self.removedCloudCluster.emit(device) + del self._remote_clusters[device.key] # Add an output device for each new remote cluster. # We only add when is_online as we don't want the option in the drop down if the cluster is not online. - for added_cluster in added_clusters: - device = CloudOutputDevice(self._api, added_cluster) - self._remote_clusters[added_cluster.cluster_id] = device - self.addedCloudCluster.emit(added_cluster) + for cluster in added_clusters: + device = CloudOutputDevice(self._api, cluster) + self._remote_clusters[cluster.cluster_id] = device + self._application.getDiscoveredPrintersModel().addDiscoveredPrinter( + cluster.cluster_id, + device.key, + cluster.friendly_name, + self._createMachineFromDiscoveredPrinter, + device.printerType, + device + ) + self.addedCloudCluster.emit(cluster) + # Update the output devices for device, cluster in updates: device.clusterData = cluster + self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter( + cluster.cluster_id, + cluster.friendly_name, + device.printerType, + ) self._connectToActiveMachine() + + def _createMachineFromDiscoveredPrinter(self, key: str) -> None: + device = self._remote_clusters[key] # type: CloudOutputDevice + if not device: + Logger.log("e", "Could not find discovered device with key [%s]", key) + return + + group_name = device.clusterData.friendly_name + machine_type_id = device.printerType + + Logger.log("i", "Creating machine from cloud device with key = [%s], group name = [%s], printer type = [%s]", + key, group_name, machine_type_id) + + # The newly added machine is automatically activated. + self._application.getMachineManager().addMachine(machine_type_id, group_name) + active_machine = CuraApplication.getInstance().getGlobalContainerStack() + if not active_machine: + return + + active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + self._connectToOutputDevice(device, active_machine) ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py index 48a4d5f031..5549da02aa 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py @@ -16,7 +16,8 @@ class CloudClusterResponse(BaseCloudModel): # \param status: The status of the cluster authentication (active or inactive). # \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, - host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, **kwargs) -> None: + host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, + friendly_name: Optional[str] = None, **kwargs) -> None: self.cluster_id = cluster_id self.host_guid = host_guid self.host_name = host_name @@ -24,6 +25,7 @@ class CloudClusterResponse(BaseCloudModel): self.is_online = is_online self.host_version = host_version self.host_internal_ip = host_internal_ip + self.friendly_name = friendly_name super().__init__(**kwargs) # Validates the model, raising an exception if the model is invalid. diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py index 2c401fab25..d11cfa8a0e 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py @@ -22,6 +22,7 @@ class TestCloudOutputDevice(TestCase): HOST_NAME = "ultimakersystem-ccbdd30044ec" HOST_GUID = "e90ae0ac-1257-4403-91ee-a44c9b7e8050" HOST_VERSION = "5.2.0" + FRIENDLY_NAME = "My Friendly Printer" STATUS_URL = "{}/connect/v1/clusters/{}/status".format(CuraCloudAPIRoot, CLUSTER_ID) PRINT_URL = "{}/connect/v1/clusters/{}/print/{}".format(CuraCloudAPIRoot, CLUSTER_ID, JOB_ID) @@ -37,7 +38,8 @@ class TestCloudOutputDevice(TestCase): patched_method.start() self.cluster = CloudClusterResponse(self.CLUSTER_ID, self.HOST_GUID, self.HOST_NAME, is_online=True, - status="active", host_version=self.HOST_VERSION) + status="active", host_version=self.HOST_VERSION, + friendly_name=self.FRIENDLY_NAME) self.network = NetworkManagerMock() self.account = MagicMock(isLoggedIn=True, accessToken="TestAccessToken") @@ -60,7 +62,7 @@ class TestCloudOutputDevice(TestCase): # We test for these in order to make sure the correct file type is selected depending on the firmware version. def test_properties(self): self.assertEqual(self.device.firmwareVersion, self.HOST_VERSION) - self.assertEqual(self.device.name, self.HOST_NAME) + self.assertEqual(self.device.name, self.FRIENDLY_NAME) def test_status(self): self.device._update()