From 3c1b3773085ed64f22e623a8dff3503cd8ef088c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 26 Jul 2019 15:07:52 +0200 Subject: [PATCH 01/52] Restructure codebase - part 1 --- plugins/UM3NetworkPrinting/__init__.py | 5 +- plugins/UM3NetworkPrinting/plugin.json | 4 +- .../src/Cloud/CloudApiClient.py | 16 +- .../src/Cloud/CloudOutputDevice.py | 16 +- .../src/Cloud/CloudOutputDeviceManager.py | 22 +- .../src/Cloud/Models/__init__.py | 2 - .../src/Cloud/ToolPathUploader.py | 4 +- .../src/DiscoverUM3Action.py | 179 ---- .../src/Factories/PrinterModelFactory.py | 30 + .../src/LegacyUM3OutputDevice.py | 647 -------------- .../src/LegacyUM3PrinterOutputController.py | 96 --- plugins/UM3NetworkPrinting/src/Models.py | 46 - .../src/{Cloud => }/Models/BaseCloudModel.py | 2 +- .../src/Models/BaseModel.py | 9 + .../Models/CloudClusterBuildPlate.py | 0 .../CloudClusterPrintCoreConfiguration.py | 0 ...CloudClusterPrintJobConfigurationChange.py | 0 .../Models/CloudClusterPrintJobConstraint.py | 0 .../Models/CloudClusterPrintJobImpediment.py | 0 .../Models/CloudClusterPrintJobStatus.py | 6 +- ...loudClusterPrinterConfigurationMaterial.py | 0 .../Models/CloudClusterPrinterStatus.py | 0 .../Models/CloudClusterResponse.py | 0 .../{Cloud => }/Models/CloudClusterStatus.py | 0 .../src/{Cloud => }/Models/CloudError.py | 0 .../Models/CloudPrintJobResponse.py | 0 .../Models/CloudPrintJobUploadRequest.py | 0 .../{Cloud => }/Models/CloudPrintResponse.py | 0 .../src/Models/ClusterMaterial.py | 15 + .../{ => Models}/ConfigurationChangeModel.py | 5 +- .../src/Models/LocalMaterial.py | 20 + .../{ => Models}/UM3PrintJobOutputModel.py | 3 +- .../UM3NetworkPrinting/src/Models/__init__.py | 0 .../{ => Network}/ClusterUM3OutputDevice.py | 101 +-- .../ClusterUM3PrinterOutputController.py | 0 .../src/Network/ManualPrinterRequest.py | 13 + .../src/Network/NetworkApiClient.py | 23 + .../src/Network/NetworkOutputDeviceManager.py | 425 ++++++++++ .../src/Network/__init__.py | 0 .../UM3NetworkPrinting/src/SendMaterialJob.py | 7 +- .../src/UM3OutputDevicePlugin.py | 801 ++++-------------- .../UltimakerNetworkedPrinterOutputDevice.py | 102 +++ .../tests/Cloud/TestCloudApiClient.py | 16 +- .../tests/Cloud/TestCloudOutputDevice.py | 6 +- .../Cloud/TestCloudOutputDeviceManager.py | 2 +- plugins/__init__.py | 0 46 files changed, 898 insertions(+), 1725 deletions(-) delete mode 100644 plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py delete mode 100644 plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py create mode 100644 plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py delete mode 100644 plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py delete mode 100644 plugins/UM3NetworkPrinting/src/LegacyUM3PrinterOutputController.py delete mode 100644 plugins/UM3NetworkPrinting/src/Models.py rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/BaseCloudModel.py (97%) create mode 100644 plugins/UM3NetworkPrinting/src/Models/BaseModel.py rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterBuildPlate.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrintCoreConfiguration.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrintJobConfigurationChange.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrintJobConstraint.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrintJobImpediment.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrintJobStatus.py (96%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrinterConfigurationMaterial.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterPrinterStatus.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterResponse.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudClusterStatus.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudError.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudPrintJobResponse.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudPrintJobUploadRequest.py (100%) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Models/CloudPrintResponse.py (100%) create mode 100644 plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py rename plugins/UM3NetworkPrinting/src/{ => Models}/ConfigurationChangeModel.py (91%) create mode 100644 plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py rename plugins/UM3NetworkPrinting/src/{ => Models}/UM3PrintJobOutputModel.py (92%) create mode 100644 plugins/UM3NetworkPrinting/src/Models/__init__.py rename plugins/UM3NetworkPrinting/src/{ => Network}/ClusterUM3OutputDevice.py (88%) rename plugins/UM3NetworkPrinting/src/{ => Network}/ClusterUM3PrinterOutputController.py (100%) create mode 100644 plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py create mode 100644 plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py create mode 100644 plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py create mode 100644 plugins/UM3NetworkPrinting/src/Network/__init__.py create mode 100644 plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py create mode 100644 plugins/__init__.py diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index 3da7795589..fd083a7afa 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from .src import DiscoverUM3Action +# from .src import DiscoverUM3Action from .src import UM3OutputDevicePlugin @@ -10,6 +10,5 @@ def getMetaData(): def register(app): return { - "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), - "machine_action": DiscoverUM3Action.DiscoverUM3Action() + "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin() } diff --git a/plugins/UM3NetworkPrinting/plugin.json b/plugins/UM3NetworkPrinting/plugin.json index 088b4dae6a..894fc41815 100644 --- a/plugins/UM3NetworkPrinting/plugin.json +++ b/plugins/UM3NetworkPrinting/plugin.json @@ -1,7 +1,7 @@ { - "name": "UM3 Network Connection", + "name": "Ultimaker Network Connection", "author": "Ultimaker B.V.", - "description": "Manages network connections to Ultimaker 3 printers.", + "description": "Manages network connections to Ultimaker networked printers.", "version": "1.0.1", "api": "6.0", "i18n-catalog": "cura" diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 30bdd8e774..9868e4a5d3 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -12,17 +12,17 @@ from UM.Logger import Logger from cura import UltimakerCloudAuthentication from cura.API import Account from .ToolPathUploader import ToolPathUploader -from ..Models import BaseModel -from .Models.CloudClusterResponse import CloudClusterResponse -from .Models.CloudError import CloudError -from .Models.CloudClusterStatus import CloudClusterStatus -from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from .Models.CloudPrintResponse import CloudPrintResponse -from .Models.CloudPrintJobResponse import CloudPrintJobResponse +from ..Models.BaseModel import BaseModel +from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse +from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError +from plugins.UM3NetworkPrinting.src.Models.CloudClusterStatus import CloudClusterStatus +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest +from plugins.UM3NetworkPrinting.src.Models.CloudPrintResponse import CloudPrintResponse +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse ## The generic type variable used to document the methods below. -CloudApiClientModel = TypeVar("CloudApiClientModel", bound = BaseModel) +CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel) ## The cloud API client is responsible for handling the requests and responses from the cloud. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index fc2cdae563..237f961acf 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -25,16 +25,16 @@ from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from .CloudOutputController import CloudOutputController from ..MeshFormatHandler import MeshFormatHandler -from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel +from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from .CloudProgressMessage import CloudProgressMessage from .CloudApiClient import CloudApiClient -from .Models.CloudClusterResponse import CloudClusterResponse -from .Models.CloudClusterStatus import CloudClusterStatus -from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from .Models.CloudPrintResponse import CloudPrintResponse -from .Models.CloudPrintJobResponse import CloudPrintJobResponse -from .Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus -from .Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus +from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse +from plugins.UM3NetworkPrinting.src.Models.CloudClusterStatus import CloudClusterStatus +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest +from plugins.UM3NetworkPrinting.src.Models.CloudPrintResponse import CloudPrintResponse +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse +from plugins.UM3NetworkPrinting.src.Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus +from plugins.UM3NetworkPrinting.src.Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus from .Utils import formatDateCompleted, formatTimeCompleted I18N_CATALOG = i18nCatalog("cura") diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index ced53e347b..55b6af8214 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -13,8 +13,8 @@ from cura.CuraApplication import CuraApplication from cura.Settings.GlobalStack import GlobalStack from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice -from .Models.CloudClusterResponse import CloudClusterResponse -from .Models.CloudError import CloudError +from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse +from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError from .Utils import findChanges @@ -52,7 +52,11 @@ class CloudOutputDeviceManager: self._running = False - # Called when the uses logs in or out + ## Force refreshing connections. + def refreshConnections(self) -> None: + pass + + ## Called when the uses logs in or out def _onLoginStateChanged(self, is_logged_in: bool) -> None: Logger.log("d", "Log in state changed to %s", is_logged_in) if is_logged_in: @@ -66,12 +70,12 @@ class CloudOutputDeviceManager: # Notify that all clusters have disappeared self._onGetRemoteClustersFinished([]) - ## Gets all remote clusters from the API. + ## Gets all remote clusters from the API. def _getRemoteClusters(self) -> None: Logger.log("d", "Retrieving remote clusters") self._api.getClusters(self._onGetRemoteClustersFinished) - ## Callback for when the request for getting the clusters. is finished. + ## Callback for when the request for getting the clusters. is finished. def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] @@ -115,19 +119,19 @@ class CloudOutputDeviceManager: ) 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() diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py deleted file mode 100644 index f3f6970c54..0000000000 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 176b7e6ab7..4faad4c6d8 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -6,7 +6,7 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManage from typing import Optional, Callable, Any, Tuple, cast from UM.Logger import Logger -from .Models.CloudPrintJobResponse import CloudPrintJobResponse +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse ## Class responsible for uploading meshes to the cloud in separate requests. @@ -53,7 +53,7 @@ class ToolPathUploader: def _createRequest(self) -> QNetworkRequest: request = QNetworkRequest(QUrl(self._print_job.upload_url)) request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type) - + first_byte, last_byte = self._chunkRange() content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data)) request.setRawHeader(b"Content-Range", content_range.encode()) diff --git a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py deleted file mode 100644 index b67f4d7185..0000000000 --- a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os.path -import time -from typing import Optional, TYPE_CHECKING - -from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject - -from UM.PluginRegistry import PluginRegistry -from UM.Logger import Logger -from UM.i18n import i18nCatalog - -from cura.CuraApplication import CuraApplication -from cura.MachineAction import MachineAction -from cura.Settings.CuraContainerRegistry import CuraContainerRegistry - -from .UM3OutputDevicePlugin import UM3OutputDevicePlugin - -if TYPE_CHECKING: - from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice - -catalog = i18nCatalog("cura") - - -class DiscoverUM3Action(MachineAction): - discoveredDevicesChanged = pyqtSignal() - - def __init__(self) -> None: - super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) - self._qml_url = "resources/qml/DiscoverUM3Action.qml" - - self._network_plugin = None #type: Optional[UM3OutputDevicePlugin] - - self.__additional_components_view = None #type: Optional[QObject] - - CuraApplication.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) - - self._last_zero_conf_event_time = time.time() #type: float - - # Time to wait after a zero-conf service change before allowing a zeroconf reset - self._zero_conf_change_grace_period = 0.25 #type: float - - # Overrides the one in MachineAction. - # This requires not attention from the user (any more), so we don't need to show any 'upgrade screens'. - def needsUserInteraction(self) -> bool: - return False - - @pyqtSlot() - def startDiscovery(self): - if not self._network_plugin: - Logger.log("d", "Starting device discovery.") - self._network_plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") - self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) - self.discoveredDevicesChanged.emit() - - ## Re-filters the list of devices. - @pyqtSlot() - def reset(self): - Logger.log("d", "Reset the list of found devices.") - if self._network_plugin: - self._network_plugin.resetLastManualDevice() - self.discoveredDevicesChanged.emit() - - @pyqtSlot() - def restartDiscovery(self): - # Ensure that there is a bit of time after a printer has been discovered. - # This is a work around for an issue with Qt 5.5.1 up to Qt 5.7 which can segfault if we do this too often. - # It's most likely that the QML engine is still creating delegates, where the python side already deleted or - # garbage collected the data. - # Whatever the case, waiting a bit ensures that it doesn't crash. - if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period: - if not self._network_plugin: - self.startDiscovery() - else: - self._network_plugin.startDiscovery() - - @pyqtSlot(str, str) - def removeManualDevice(self, key, address): - if not self._network_plugin: - return - - self._network_plugin.removeManualDevice(key, address) - - @pyqtSlot(str, str) - def setManualDevice(self, key, address): - if key != "": - # This manual printer replaces a current manual printer - self._network_plugin.removeManualDevice(key) - - if address != "": - self._network_plugin.addManualDevice(address) - - def _onDeviceDiscoveryChanged(self, *args): - self._last_zero_conf_event_time = time.time() - self.discoveredDevicesChanged.emit() - - @pyqtProperty("QVariantList", notify = discoveredDevicesChanged) - def foundDevices(self): - if self._network_plugin: - - printers = list(self._network_plugin.getDiscoveredDevices().values()) - printers.sort(key = lambda k: k.name) - return printers - else: - return [] - - @pyqtSlot(str) - def setGroupName(self, group_name: str) -> None: - Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name) - global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() - if global_container_stack: - # Update a GlobalStacks in the same group with the new group name. - group_id = global_container_stack.getMetaDataEntry("group_id") - machine_manager = CuraApplication.getInstance().getMachineManager() - for machine in machine_manager.getMachinesInGroup(group_id): - machine.setMetaDataEntry("group_name", group_name) - - # Set the default value for "hidden", which is used when you have a group with multiple types of printers - global_container_stack.setMetaDataEntry("hidden", False) - - if self._network_plugin: - # Ensure that the connection states are refreshed. - self._network_plugin.refreshConnections() - - # Associates the currently active machine with the given printer device. The network connection information will be - # stored into the metadata of the currently active machine. - @pyqtSlot(QObject) - def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None: - if self._network_plugin: - self._network_plugin.associateActiveMachineWithPrinterDevice(printer_device) - - @pyqtSlot(result = str) - def getStoredKey(self) -> str: - global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() - if global_container_stack: - meta_data = global_container_stack.getMetaData() - if "um_network_key" in meta_data: - return global_container_stack.getMetaDataEntry("um_network_key") - - return "" - - @pyqtSlot(result = str) - def getLastManualEntryKey(self) -> str: - if self._network_plugin: - return self._network_plugin.getLastManualDevice() - return "" - - @pyqtSlot(str, result = bool) - def existsKey(self, key: str) -> bool: - metadata_filter = {"um_network_key": key} - containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine", **metadata_filter) - return bool(containers) - - @pyqtSlot() - def loadConfigurationFromPrinter(self) -> None: - machine_manager = CuraApplication.getInstance().getMachineManager() - hotend_ids = machine_manager.printerOutputDevices[0].hotendIds - for index in range(len(hotend_ids)): - machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index]) - material_ids = machine_manager.printerOutputDevices[0].materialIds - for index in range(len(material_ids)): - machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index]) - - def _createAdditionalComponentsView(self) -> None: - Logger.log("d", "Creating additional ui components for UM3.") - - # Create networking dialog - plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") - if not plugin_path: - return - path = os.path.join(plugin_path, "resources/qml/UM3InfoComponents.qml") - self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) - if not self.__additional_components_view: - Logger.log("w", "Could not create ui components for UM3.") - return - - # Create extra components - CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) diff --git a/plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py b/plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py new file mode 100644 index 0000000000..df6903ec71 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py @@ -0,0 +1,30 @@ +from typing import List, Any, Dict + +from PyQt5.QtCore import QUrl + +from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.PrinterOutputController import PrinterOutputController +from plugins.UM3NetworkPrinting.src.Models.ConfigurationChangeModel import ConfigurationChangeModel + + +class PrinterModelFactory: + + CAMERA_URL_FORMAT = "http://{ip_address}:8080/?action=stream" + + # Create a printer output model from some data. + @classmethod + def createPrinter(cls, output_controller: PrinterOutputController, ip_address: str, extruder_count: int = 2 + ) -> PrinterOutputModel: + printer = PrinterOutputModel(output_controller=output_controller, number_of_extruders=extruder_count) + printer.setCameraUrl(QUrl(cls.CAMERA_URL_FORMAT.format(ip_address=ip_address))) + return printer + + # Create a list of configuration change models. + @classmethod + def createConfigurationChanges(cls, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]: + return [ConfigurationChangeModel( + type_of_change=change.get("type_of_change"), + index=change.get("index"), + target_name=change.get("target_name"), + origin_name=change.get("origin_name") + ) for change in data] diff --git a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py deleted file mode 100644 index 7d759264e5..0000000000 --- a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py +++ /dev/null @@ -1,647 +0,0 @@ -from typing import List, Optional - -from cura.CuraApplication import CuraApplication -from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState -from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel -from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel -from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel -from cura.PrinterOutput.PrinterOutputDevice import ConnectionType - -from cura.Settings.ContainerManager import ContainerManager -from cura.Settings.ExtruderManager import ExtruderManager - -from UM.FileHandler.FileHandler import FileHandler -from UM.i18n import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message -from UM.PluginRegistry import PluginRegistry -from UM.Scene.SceneNode import SceneNode -from UM.Settings.ContainerRegistry import ContainerRegistry - -from PyQt5.QtNetwork import QNetworkRequest -from PyQt5.QtCore import QTimer, QUrl -from PyQt5.QtWidgets import QMessageBox - -from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController - -from time import time - -import json -import os - - -i18n_catalog = i18nCatalog("cura") - - -## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API. -# Everything after that firmware uses the ClusterUM3Output. -# The Legacy output device can only have one printer (whereas the cluster can have 0 to n). -# -# Authentication is done in a number of steps; -# 1. Request an id / key pair by sending the application & user name. (state = authRequested) -# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived) -# 3. OutputDevice will poll if the button was pressed. -# 4. At this point the machine either has the state Authenticated or AuthenticationDenied. -# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator. -class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): - def __init__(self, device_id, address: str, properties, parent = None) -> None: - super().__init__(device_id = device_id, address = address, properties = properties, connection_type = ConnectionType.NetworkConnection, parent = parent) - self._api_prefix = "/api/v1/" - self._number_of_extruders = 2 - - self._authentication_id = None - self._authentication_key = None - - self._authentication_counter = 0 - self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) - - self._authentication_timer = QTimer() - self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval - self._authentication_timer.setSingleShot(False) - - self._authentication_timer.timeout.connect(self._onAuthenticationTimer) - - # The messages are created when connect is called the first time. - # This ensures that the messages are only created for devices that actually want to connect. - self._authentication_requested_message = None - self._authentication_failed_message = None - self._authentication_succeeded_message = None - self._not_authenticated_message = None - - self.authenticationStateChanged.connect(self._onAuthenticationStateChanged) - - self.setPriority(3) # Make sure the output device gets selected above local file output - self.setName(self._id) - self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) - self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) - - self.setIconName("print") - - self._output_controller = LegacyUM3PrinterOutputController(self) - - def _createMonitorViewFromQML(self) -> None: - if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None: - self._monitor_view_qml_path = os.path.join( - PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - "resources", "qml", "MonitorStage.qml" - ) - super()._createMonitorViewFromQML() - - def _onAuthenticationStateChanged(self): - # We only accept commands if we are authenticated. - self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated) - - if self._authentication_state == AuthState.Authenticated: - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network.")) - elif self._authentication_state == AuthState.AuthenticationRequested: - self.setConnectionText(i18n_catalog.i18nc("@info:status", - "Connected over the network. Please approve the access request on the printer.")) - elif self._authentication_state == AuthState.AuthenticationDenied: - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer.")) - - - def _setupMessages(self): - self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", - "Access to the printer requested. Please approve the request on the printer"), - lifetime=0, dismissable=False, progress=0, - title=i18n_catalog.i18nc("@info:title", - "Authentication status")) - - self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status")) - self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, - i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) - self._authentication_failed_message.actionTriggered.connect(self._messageCallback) - self._authentication_succeeded_message = Message( - i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), - title=i18n_catalog.i18nc("@info:title", "Authentication Status")) - - self._not_authenticated_message = Message( - i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."), - title=i18n_catalog.i18nc("@info:title", "Authentication Status")) - self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"), - None, i18n_catalog.i18nc("@info:tooltip", - "Send access request to the printer")) - self._not_authenticated_message.actionTriggered.connect(self._messageCallback) - - def _messageCallback(self, message_id=None, action_id="Retry"): - if action_id == "Request" or action_id == "Retry": - if self._authentication_failed_message: - self._authentication_failed_message.hide() - if self._not_authenticated_message: - self._not_authenticated_message.hide() - - self._requestAuthentication() - - def connect(self): - super().connect() - self._setupMessages() - global_container = CuraApplication.getInstance().getGlobalContainerStack() - if global_container: - self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None) - self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None) - - def close(self): - super().close() - if self._authentication_requested_message: - self._authentication_requested_message.hide() - if self._authentication_failed_message: - self._authentication_failed_message.hide() - if self._authentication_succeeded_message: - self._authentication_succeeded_message.hide() - self._sending_gcode = False - self._compressing_gcode = False - self._authentication_timer.stop() - - ## Send all material profiles to the printer. - def _sendMaterialProfiles(self): - Logger.log("i", "Sending material profiles to printer") - - # TODO: Might want to move this to a job... - for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"): - try: - xml_data = container.serialize() - if xml_data == "" or xml_data is None: - continue - - names = ContainerManager.getInstance().getLinkedMaterials(container.getId()) - if names: - # There are other materials that share this GUID. - if not container.isReadOnly(): - continue # If it's not readonly, it's created by user, so skip it. - - file_name = "none.xml" - - self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), on_finished=None) - - except NotImplementedError: - # If the material container is not the most "generic" one it can't be serialized an will raise a - # NotImplementedError. We can simply ignore these. - pass - - def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: - if not self.activePrinter: - # No active printer. Unable to write - return - - if self.activePrinter.state not in ["idle", ""]: - # Printer is not able to accept commands. - return - - if self._authentication_state != AuthState.Authenticated: - # Not authenticated, so unable to send job. - return - - self.writeStarted.emit(self) - - gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", []) - active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate - gcode_list = gcode_dict[active_build_plate_id] - - if not gcode_list: - # Unable to find g-code. Nothing to send - return - - self._gcode = gcode_list - - errors = self._checkForErrors() - if errors: - text = i18n_catalog.i18nc("@label", "Unable to start a new print job.") - informative_text = i18n_catalog.i18nc("@label", - "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. " - "Please resolve this issues before continuing.") - detailed_text = "" - for error in errors: - detailed_text += error + "\n" - - CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), - text, - informative_text, - detailed_text, - buttons=QMessageBox.Ok, - icon=QMessageBox.Critical, - callback = self._messageBoxCallback - ) - return # Don't continue; Errors must block sending the job to the printer. - - # There might be multiple things wrong with the configuration. Check these before starting. - warnings = self._checkForWarnings() - - if warnings: - text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") - informative_text = i18n_catalog.i18nc("@label", - "There is a mismatch between the configuration or calibration of the printer and Cura. " - "For the best result, always slice for the PrintCores and materials that are inserted in your printer.") - detailed_text = "" - for warning in warnings: - detailed_text += warning + "\n" - - CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), - text, - informative_text, - detailed_text, - buttons=QMessageBox.Yes + QMessageBox.No, - icon=QMessageBox.Question, - callback=self._messageBoxCallback - ) - return - - # No warnings or errors, so we're good to go. - self._startPrint() - - # Notify the UI that a switch to the print monitor should happen - CuraApplication.getInstance().getController().setActiveStage("MonitorStage") - - def _startPrint(self): - Logger.log("i", "Sending print job to printer.") - if self._sending_gcode: - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Sending new jobs (temporarily) blocked, still sending the previous print job.")) - self._error_message.show() - return - - self._sending_gcode = True - - self._send_gcode_start = time() - self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, - i18n_catalog.i18nc("@info:title", "Sending Data")) - self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") - self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) - self._progress_message.show() - - compressed_gcode = self._compressGCode() - if compressed_gcode is None: - # Abort was called. - return - - file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName - self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, - on_finished=self._onPostPrintJobFinished) - - return - - def _progressMessageActionTriggered(self, message_id=None, action_id=None): - if action_id == "Abort": - Logger.log("d", "User aborted sending print to remote.") - self._progress_message.hide() - self._compressing_gcode = False - self._sending_gcode = False - CuraApplication.getInstance().getController().setActiveStage("PrepareStage") - - def _onPostPrintJobFinished(self, reply): - self._progress_message.hide() - self._sending_gcode = False - - def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): - if bytes_total > 0: - new_progress = bytes_sent / bytes_total * 100 - # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get - # timeout responses if this happens. - self._last_response_time = time() - if new_progress > self._progress_message.getProgress(): - self._progress_message.show() # Ensure that the message is visible. - self._progress_message.setProgress(bytes_sent / bytes_total * 100) - else: - self._progress_message.setProgress(0) - - self._progress_message.hide() - - def _messageBoxCallback(self, button): - def delayedCallback(): - if button == QMessageBox.Yes: - self._startPrint() - else: - CuraApplication.getInstance().getController().setActiveStage("PrepareStage") - # For some unknown reason Cura on OSX will hang if we do the call back code - # immediately without first returning and leaving QML's event system. - - QTimer.singleShot(100, delayedCallback) - - def _checkForErrors(self): - errors = [] - print_information = CuraApplication.getInstance().getPrintInformation() - if not print_information.materialLengths: - Logger.log("w", "There is no material length information. Unable to check for errors.") - return errors - - for index, extruder in enumerate(self.activePrinter.extruders): - # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not. - if extruder.hotendID == "": - # No Printcore loaded. - errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1))) - - if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: - # The extruder is by this print. - if extruder.activeMaterial is None: - # No active material - errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1))) - return errors - - def _checkForWarnings(self): - warnings = [] - print_information = CuraApplication.getInstance().getPrintInformation() - - if not print_information.materialLengths: - Logger.log("w", "There is no material length information. Unable to check for warnings.") - return warnings - - extruder_manager = ExtruderManager.getInstance() - - for index, extruder in enumerate(self.activePrinter.extruders): - if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: - # The extruder is by this print. - - # TODO: material length check - - # Check if the right Printcore is active. - variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) - if variant: - if variant.getName() != extruder.hotendID: - warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1))) - else: - Logger.log("w", "Unable to find variant.") - - # Check if the right material is loaded. - local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) - if local_material: - if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"): - Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID")) - warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1)) - else: - Logger.log("w", "Unable to find material.") - - return warnings - - def _update(self): - if not super()._update(): - return - if self._authentication_state == AuthState.NotAuthenticated: - if self._authentication_id is None and self._authentication_key is None: - # This machine doesn't have any authentication, so request it. - self._requestAuthentication() - elif self._authentication_id is not None and self._authentication_key is not None: - # We have authentication info, but we haven't checked it out yet. Do so now. - self._verifyAuthentication() - elif self._authentication_state == AuthState.AuthenticationReceived: - # We have an authentication, but it's not confirmed yet. - self._checkAuthentication() - - # We don't need authentication for requesting info, so we can go right ahead with requesting this. - self.get("printer", on_finished=self._onGetPrinterDataFinished) - self.get("print_job", on_finished=self._onGetPrintJobFinished) - - def _resetAuthenticationRequestedMessage(self): - if self._authentication_requested_message: - self._authentication_requested_message.hide() - self._authentication_timer.stop() - self._authentication_counter = 0 - - def _onAuthenticationTimer(self): - self._authentication_counter += 1 - self._authentication_requested_message.setProgress( - self._authentication_counter / self._max_authentication_counter * 100) - if self._authentication_counter > self._max_authentication_counter: - self._authentication_timer.stop() - Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id) - self.setAuthenticationState(AuthState.AuthenticationDenied) - self._resetAuthenticationRequestedMessage() - self._authentication_failed_message.show() - - def _verifyAuthentication(self): - Logger.log("d", "Attempting to verify authentication") - # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator. - self.get("auth/verify", on_finished=self._onVerifyAuthenticationCompleted) - - def _onVerifyAuthenticationCompleted(self, reply): - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if status_code == 401: - # Something went wrong; We somehow tried to verify authentication without having one. - Logger.log("d", "Attempted to verify auth without having one.") - self._authentication_id = None - self._authentication_key = None - self.setAuthenticationState(AuthState.NotAuthenticated) - elif status_code == 403 and self._authentication_state != AuthState.Authenticated: - # If we were already authenticated, we probably got an older message back all of the sudden. Drop that. - Logger.log("d", - "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", - self._authentication_state) - self.setAuthenticationState(AuthState.AuthenticationDenied) - self._authentication_failed_message.show() - elif status_code == 200: - self.setAuthenticationState(AuthState.Authenticated) - - def _checkAuthentication(self): - Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - self.get("auth/check/" + str(self._authentication_id), on_finished=self._onCheckAuthenticationFinished) - - def _onCheckAuthenticationFinished(self, reply): - if str(self._authentication_id) not in reply.url().toString(): - Logger.log("w", "Got an old id response.") - # Got response for old authentication ID. - return - try: - data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.") - return - - if data.get("message", "") == "authorized": - Logger.log("i", "Authentication was approved") - self.setAuthenticationState(AuthState.Authenticated) - self._saveAuthentication() - - # Double check that everything went well. - self._verifyAuthentication() - - # Notify the user. - self._resetAuthenticationRequestedMessage() - self._authentication_succeeded_message.show() - elif data.get("message", "") == "unauthorized": - Logger.log("i", "Authentication was denied.") - self.setAuthenticationState(AuthState.AuthenticationDenied) - self._authentication_failed_message.show() - - def _saveAuthentication(self) -> None: - global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() - if self._authentication_key is None: - Logger.log("e", "Authentication key is None, nothing to save.") - return - if self._authentication_id is None: - Logger.log("e", "Authentication id is None, nothing to save.") - return - if global_container_stack: - global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key) - - global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id) - - # Force save so we are sure the data is not lost. - CuraApplication.getInstance().saveStack(global_container_stack) - Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, - self._getSafeAuthKey()) - else: - Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id, - self._getSafeAuthKey()) - - def _onRequestAuthenticationFinished(self, reply): - try: - data = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.") - self.setAuthenticationState(AuthState.NotAuthenticated) - return - - self.setAuthenticationState(AuthState.AuthenticationReceived) - self._authentication_id = data["id"] - self._authentication_key = data["key"] - Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", - self._authentication_id, self._getSafeAuthKey()) - - def _requestAuthentication(self): - self._authentication_requested_message.show() - self._authentication_timer.start() - - # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might - # give issues. - self._authentication_key = None - self._authentication_id = None - - self.post("auth/request", - json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(), - "user": self._getUserName()}), - on_finished=self._onRequestAuthenticationFinished) - - self.setAuthenticationState(AuthState.AuthenticationRequested) - - def _onAuthenticationRequired(self, reply, authenticator): - if self._authentication_id is not None and self._authentication_key is not None: - Logger.log("d", - "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s", - self._id, self._authentication_id, self._getSafeAuthKey()) - authenticator.setUser(self._authentication_id) - authenticator.setPassword(self._authentication_key) - else: - Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id) - - def _onGetPrintJobFinished(self, reply): - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - - if not self._printers: - return # Ignore the data for now, we don't have info about a printer yet. - printer = self._printers[0] - - if status_code == 200: - try: - result = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid print job state message: Not valid JSON.") - return - if printer.activePrintJob is None: - print_job = PrintJobOutputModel(output_controller=self._output_controller) - printer.updateActivePrintJob(print_job) - else: - print_job = printer.activePrintJob - print_job.updateState(result["state"]) - print_job.updateTimeElapsed(result["time_elapsed"]) - print_job.updateTimeTotal(result["time_total"]) - print_job.updateName(result["name"]) - elif status_code == 404: - # No job found, so delete the active print job (if any!) - printer.updateActivePrintJob(None) - else: - Logger.log("w", - "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) - - def materialHotendChangedMessage(self, callback): - CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"), - i18n_catalog.i18nc("@label", - "Would you like to use your current printer configuration in Cura?"), - i18n_catalog.i18nc("@label", - "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."), - buttons=QMessageBox.Yes + QMessageBox.No, - icon=QMessageBox.Question, - callback=callback - ) - - def _onGetPrinterDataFinished(self, reply): - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if status_code == 200: - try: - result = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.log("w", "Received an invalid printer state message: Not valid JSON.") - return - - if not self._printers: - # Quickest way to get the firmware version is to grab it from the zeroconf. - firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8") - self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)] - self._printers[0].setCameraUrl(QUrl("http://" + self._address + ":8080/?action=stream")) - for extruder in self._printers[0].extruders: - extruder.activeMaterialChanged.connect(self.materialIdChanged) - extruder.hotendIDChanged.connect(self.hotendIdChanged) - self.printersChanged.emit() - - # LegacyUM3 always has a single printer. - printer = self._printers[0] - printer.updateBedTemperature(result["bed"]["temperature"]["current"]) - printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"]) - printer.updateState(result["status"]) - - try: - # If we're still handling the request, we should ignore remote for a bit. - if not printer.getController().isPreheatRequestInProgress(): - printer.updateIsPreheating(result["bed"]["pre_heat"]["active"]) - except KeyError: - # Older firmwares don't support preheating, so we need to fake it. - pass - - head_position = result["heads"][0]["position"] - printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"]) - - for index in range(0, self._number_of_extruders): - temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"] - extruder = printer.extruders[index] - extruder.updateTargetHotendTemperature(temperatures["target"]) - extruder.updateHotendTemperature(temperatures["current"]) - - material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"] - - if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid: - # Find matching material (as we need to set brand, type & color) - containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", - GUID=material_guid) - if containers: - color = containers[0].getMetaDataEntry("color_code") - brand = containers[0].getMetaDataEntry("brand") - material_type = containers[0].getMetaDataEntry("material") - name = containers[0].getName() - else: - # Unknown material. - color = "#00000000" - brand = "Unknown" - material_type = "Unknown" - name = "Unknown" - material = MaterialOutputModel(guid=material_guid, type=material_type, - brand=brand, color=color, name = name) - extruder.updateActiveMaterial(material) - - try: - hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"] - except KeyError: - hotend_id = "" - printer.extruders[index].updateHotendID(hotend_id) - - else: - Logger.log("w", - "Got status code {status_code} while trying to get printer data".format(status_code = status_code)) - - ## Convenience function to "blur" out all but the last 5 characters of the auth key. - # This can be used to debug print the key, without it compromising the security. - def _getSafeAuthKey(self): - if self._authentication_key is not None: - result = self._authentication_key[-5:] - result = "********" + result - return result - - return self._authentication_key diff --git a/plugins/UM3NetworkPrinting/src/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/src/LegacyUM3PrinterOutputController.py deleted file mode 100644 index 9e372d4113..0000000000 --- a/plugins/UM3NetworkPrinting/src/LegacyUM3PrinterOutputController.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from cura.PrinterOutput.PrinterOutputController import PrinterOutputController -from PyQt5.QtCore import QTimer -from UM.Version import Version - -MYPY = False -if MYPY: - from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel - from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel - - -class LegacyUM3PrinterOutputController(PrinterOutputController): - def __init__(self, output_device): - super().__init__(output_device) - self._preheat_bed_timer = QTimer() - self._preheat_bed_timer.setSingleShot(True) - self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) - self._preheat_printer = None - - self.can_control_manually = False - self.can_send_raw_gcode = False - - # Are we still waiting for a response about preheat? - # We need this so we can already update buttons, so it feels more snappy. - self._preheat_request_in_progress = False - - def isPreheatRequestInProgress(self): - return self._preheat_request_in_progress - - def setJobState(self, job: "PrintJobOutputModel", state: str): - data = "{\"target\": \"%s\"}" % state - self._output_device.put("print_job/state", data, on_finished=None) - - def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float): - data = str(temperature) - self._output_device.put("printer/bed/temperature/target", data, on_finished = self._onPutBedTemperatureCompleted) - - def _onPutBedTemperatureCompleted(self, reply): - if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"): - # If it was handling a preheat, it isn't anymore. - self._preheat_request_in_progress = False - - def _onPutPreheatBedCompleted(self, reply): - self._preheat_request_in_progress = False - - def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): - head_pos = printer._head_position - new_x = head_pos.x + x - new_y = head_pos.y + y - new_z = head_pos.z + z - data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z) - self._output_device.put("printer/heads/0/position", data, on_finished=None) - - def homeBed(self, printer): - self._output_device.put("printer/heads/0/position/z", "0", on_finished=None) - - def _onPreheatBedTimerFinished(self): - self.setTargetBedTemperature(self._preheat_printer, 0) - self._preheat_printer.updateIsPreheating(False) - self._preheat_request_in_progress = True - - def cancelPreheatBed(self, printer: "PrinterOutputModel"): - self.preheatBed(printer, temperature=0, duration=0) - self._preheat_bed_timer.stop() - printer.updateIsPreheating(False) - - def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): - try: - temperature = round(temperature) # The API doesn't allow floating point. - duration = round(duration) - except ValueError: - return # Got invalid values, can't pre-heat. - - if duration > 0: - data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration) - else: - data = """{"temperature": "%i"}""" % temperature - - # Real bed pre-heating support is implemented from 3.5.92 and up. - - if Version(printer.firmwareVersion) < Version("3.5.92"): - # No firmware-side duration support then, so just set target bed temp and set a timer. - self.setTargetBedTemperature(printer, temperature=temperature) - self._preheat_bed_timer.setInterval(duration * 1000) - self._preheat_bed_timer.start() - self._preheat_printer = printer - printer.updateIsPreheating(True) - return - - self._output_device.put("printer/bed/pre_heat", data, on_finished = self._onPutPreheatBedCompleted) - printer.updateIsPreheating(True) - self._preheat_request_in_progress = True - - diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py deleted file mode 100644 index c5b9b16665..0000000000 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - - -## Base model that maps kwargs to instance attributes. -class BaseModel: - def __init__(self, **kwargs) -> None: - self.__dict__.update(kwargs) - self.validate() - - # Validates the model, raising an exception if the model is invalid. - def validate(self) -> None: - pass - - -## Class representing a material that was fetched from the cluster API. -class ClusterMaterial(BaseModel): - def __init__(self, guid: str, version: int, **kwargs) -> None: - self.guid = guid # type: str - self.version = version # type: int - super().__init__(**kwargs) - - def validate(self) -> None: - if not self.guid: - raise ValueError("guid is required on ClusterMaterial") - if not self.version: - raise ValueError("version is required on ClusterMaterial") - - -## Class representing a local material that was fetched from the container registry. -class LocalMaterial(BaseModel): - def __init__(self, GUID: str, id: str, version: int, **kwargs) -> None: - self.GUID = GUID # type: str - self.id = id # type: str - self.version = version # type: int - super().__init__(**kwargs) - - # - def validate(self) -> None: - super().validate() - if not self.GUID: - raise ValueError("guid is required on LocalMaterial") - if not self.version: - raise ValueError("version is required on LocalMaterial") - if not self.id: - raise ValueError("id is required on LocalMaterial") diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/BaseCloudModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py similarity index 97% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/BaseCloudModel.py rename to plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py index 18a8cb5cba..af7115d738 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/BaseCloudModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from typing import Dict, Union, TypeVar, Type, List, Any -from ...Models import BaseModel +from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel ## Base class for the models used in the interface with the Ultimaker cloud APIs. diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py new file mode 100644 index 0000000000..a48a9f838e --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -0,0 +1,9 @@ +## Base model that maps kwargs to instance attributes. +class BaseModel: + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + self.validate() + + # Validates the model, raising an exception if the model is invalid. + def validate(self) -> None: + pass diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterBuildPlate.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterBuildPlate.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterBuildPlate.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintCoreConfiguration.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintCoreConfiguration.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintCoreConfiguration.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConfigurationChange.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConfigurationChange.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConfigurationChange.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConstraint.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConstraint.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConstraint.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobImpediment.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobImpediment.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobImpediment.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobStatus.py similarity index 96% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobStatus.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobStatus.py index 4a3823ccca..30f9b137f9 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobStatus.py @@ -3,9 +3,9 @@ from typing import List, Optional, Union, Dict, Any from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel -from ...UM3PrintJobOutputModel import UM3PrintJobOutputModel -from ...ConfigurationChangeModel import ConfigurationChangeModel -from ..CloudOutputController import CloudOutputController +from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel +from plugins.UM3NetworkPrinting.src.Models.ConfigurationChangeModel import ConfigurationChangeModel +from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController from .BaseCloudModel import BaseCloudModel from .CloudClusterBuildPlate import CloudClusterBuildPlate from .CloudClusterPrintJobConfigurationChange import CloudClusterPrintJobConfigurationChange diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterConfigurationMaterial.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfigurationMaterial.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterConfigurationMaterial.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterStatus.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterStatus.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterResponse.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterResponse.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/CloudClusterStatus.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterStatus.py rename to plugins/UM3NetworkPrinting/src/Models/CloudClusterStatus.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudError.py b/plugins/UM3NetworkPrinting/src/Models/CloudError.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudError.py rename to plugins/UM3NetworkPrinting/src/Models/CloudError.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/CloudPrintJobResponse.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobResponse.py rename to plugins/UM3NetworkPrinting/src/Models/CloudPrintJobResponse.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Models/CloudPrintJobUploadRequest.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobUploadRequest.py rename to plugins/UM3NetworkPrinting/src/Models/CloudPrintJobUploadRequest.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Models/CloudPrintResponse.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintResponse.py rename to plugins/UM3NetworkPrinting/src/Models/CloudPrintResponse.py diff --git a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py new file mode 100644 index 0000000000..37e4ed390f --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py @@ -0,0 +1,15 @@ +## Class representing a material that was fetched from the cluster API. +from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel + + +class ClusterMaterial(BaseModel): + def __init__(self, guid: str, version: int, **kwargs) -> None: + self.guid = guid # type: str + self.version = version # type: int + super().__init__(**kwargs) + + def validate(self) -> None: + if not self.guid: + raise ValueError("guid is required on ClusterMaterial") + if not self.version: + raise ValueError("version is required on ClusterMaterial") diff --git a/plugins/UM3NetworkPrinting/src/ConfigurationChangeModel.py b/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py similarity index 91% rename from plugins/UM3NetworkPrinting/src/ConfigurationChangeModel.py rename to plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py index 7136d8b93f..3521b55f63 100644 --- a/plugins/UM3NetworkPrinting/src/ConfigurationChangeModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py @@ -1,12 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot +from PyQt5.QtCore import pyqtProperty, QObject BLOCKING_CHANGE_TYPES = [ "material_insert", "buildplate_change" ] + class ConfigurationChangeModel(QObject): def __init__(self, type_of_change: str, index: int, target_name: str, origin_name: str) -> None: super().__init__() @@ -35,4 +36,4 @@ class ConfigurationChangeModel(QObject): @pyqtProperty(bool, constant = True) def canOverride(self) -> bool: - return self._can_override \ No newline at end of file + return self._can_override diff --git a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py new file mode 100644 index 0000000000..f3c3772a09 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py @@ -0,0 +1,20 @@ +## Class representing a local material that was fetched from the container registry. +from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel + + +class LocalMaterial(BaseModel): + def __init__(self, GUID: str, id: str, version: int, **kwargs) -> None: + self.GUID = GUID # type: str + self.id = id # type: str + self.version = version # type: int + super().__init__(**kwargs) + + # + def validate(self) -> None: + super().validate() + if not self.GUID: + raise ValueError("guid is required on LocalMaterial") + if not self.version: + raise ValueError("version is required on LocalMaterial") + if not self.id: + raise ValueError("id is required on LocalMaterial") diff --git a/plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py similarity index 92% rename from plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py rename to plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py index b627b6e9c8..0112ab94eb 100644 --- a/plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py @@ -1,13 +1,12 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - from typing import List from PyQt5.QtCore import pyqtProperty, pyqtSignal from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController -from .ConfigurationChangeModel import ConfigurationChangeModel +from plugins.UM3NetworkPrinting.src.Models.ConfigurationChangeModel import ConfigurationChangeModel class UM3PrintJobOutputModel(PrintJobOutputModel): diff --git a/plugins/UM3NetworkPrinting/src/Models/__init__.py b/plugins/UM3NetworkPrinting/src/Models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py similarity index 88% rename from plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py rename to plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py index 177836bccd..f39c4b2d9b 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py @@ -21,17 +21,17 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from cura.CuraApplication import CuraApplication from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel -from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from plugins.UM3NetworkPrinting.src.Factories.PrinterModelFactory import PrinterModelFactory +from plugins.UM3NetworkPrinting.src.UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice -from .Cloud.Utils import formatTimeCompleted, formatDateCompleted -from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController -from .ConfigurationChangeModel import ConfigurationChangeModel -from .MeshFormatHandler import MeshFormatHandler -from .SendMaterialJob import SendMaterialJob -from .UM3PrintJobOutputModel import UM3PrintJobOutputModel +from plugins.UM3NetworkPrinting.src.Cloud.Utils import formatTimeCompleted, formatDateCompleted +from plugins.UM3NetworkPrinting.src.Network.ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController +from plugins.UM3NetworkPrinting.src.MeshFormatHandler import MeshFormatHandler +from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob +from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices, QImage @@ -40,16 +40,11 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject i18n_catalog = i18nCatalog("cura") -class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): - printJobsChanged = pyqtSignal() - activePrinterChanged = pyqtSignal() +class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): + activeCameraUrlChanged = pyqtSignal() receivedPrintJobsChanged = pyqtSignal() - # Notify can only use signals that are defined by the class that they are in, not inherited ones. - # Therefore we create a private signal used to trigger the printersChanged signal. - _clusterPrintersChanged = pyqtSignal() - def __init__(self, device_id, address, properties, parent = None) -> None: super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent) self._api_prefix = "/cluster-api/v1/" @@ -62,7 +57,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): "", {}, io.BytesIO() ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]] - self._print_jobs = [] # type: List[UM3PrintJobOutputModel] self._received_print_jobs = False # type: bool if PluginRegistry.getInstance() is not None: @@ -77,15 +71,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._accepts_commands = True # type: bool - # Cluster does not have authentication, so default to authenticated - self._authentication_state = AuthState.Authenticated - self._error_message = None # type: Optional[Message] self._write_job_progress_message = None # type: Optional[Message] self._progress_message = None # type: Optional[Message] - self._active_printer = None # type: Optional[PrinterOutputModel] - self._printer_selection_dialog = None # type: QObject self.setPriority(3) # Make sure the output device gets selected above local file output @@ -145,10 +134,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def supportsPrintJobActions(self) -> bool: return True - @pyqtProperty(int, constant=True) - def clusterSize(self) -> int: - return self._cluster_size - ## Allows the user to choose a printer to print with from the printer # selection dialogue. # \param target_printer The name of the printer to target. @@ -240,16 +225,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress) - @pyqtProperty(QObject, notify = activePrinterChanged) - def activePrinter(self) -> Optional[PrinterOutputModel]: - return self._active_printer - - @pyqtSlot(QObject) - def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: - if self._active_printer != printer: - self._active_printer = printer - self.activePrinterChanged.emit() - @pyqtProperty(QUrl, notify = activeCameraUrlChanged) def activeCameraUrl(self) -> "QUrl": return self._active_camera_url @@ -317,49 +292,20 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if action_id == "View": self._application.getController().setActiveStage("MonitorStage") - @pyqtSlot() + @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs")) - @pyqtSlot() + @pyqtSlot(name="openPrinterControlPanel") def openPrinterControlPanel(self) -> None: Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) - @pyqtProperty("QVariantList", notify = printJobsChanged) - def printJobs(self)-> List[UM3PrintJobOutputModel]: - return self._print_jobs - @pyqtProperty(bool, notify = receivedPrintJobsChanged) def receivedPrintJobs(self) -> bool: return self._received_print_jobs - @pyqtProperty("QVariantList", notify = printJobsChanged) - def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]: - return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"] - - @pyqtProperty("QVariantList", notify = printJobsChanged) - def activePrintJobs(self) -> List[UM3PrintJobOutputModel]: - return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"] - - @pyqtProperty("QVariantList", notify = _clusterPrintersChanged) - def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: - printer_count = {} # type: Dict[str, int] - for printer in self._printers: - if printer.type in printer_count: - printer_count[printer.type] += 1 - else: - printer_count[printer.type] = 1 - result = [] - for machine_type in printer_count: - result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])}) - return result - - @pyqtProperty("QVariantList", notify=_clusterPrintersChanged) - def printers(self): - return self._printers - @pyqtSlot(int, result = str) def getTimeCompleted(self, time_remaining: int) -> str: return formatTimeCompleted(time_remaining) @@ -510,7 +456,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printer = findByKey(self._printers, printer_data["uuid"]) if printer is None: - printer = self._createPrinterModel(printer_data) + output_controller = ClusterUM3PrinterOutputController(self) + printer = PrinterModelFactory.createPrinter(output_controller=output_controller, + ip_address=printer_data.get("ip_address", ""), + extruder_count=self._number_of_extruders) + self._printers.append(printer) printer_list_changed = True printers_seen.append(printer) @@ -524,13 +474,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if removed_printers or printer_list_changed: self.printersChanged.emit() - def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel: - printer = PrinterOutputModel(output_controller = ClusterUM3PrinterOutputController(self), - number_of_extruders = self._number_of_extruders) - printer.setCameraUrl(QUrl("http://" + data["ip_address"] + ":8080/?action=stream")) - self._printers.append(printer) - return printer - def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel: print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name= data["name"]) @@ -569,16 +512,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if not status_set_by_impediment: print_job.updateState(data["status"]) - print_job.updateConfigurationChanges(self._createConfigurationChanges(data["configuration_changes_required"])) - - def _createConfigurationChanges(self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]: - result = [] - for change in data: - result.append(ConfigurationChangeModel(type_of_change=change["type_of_change"], - index=change["index"], - target_name=change["target_name"], - origin_name=change["origin_name"])) - return result + configuration_changes = PrinterModelFactory.createConfigurationChanges(data.get("configuration_changes_required")) + print_job.updateConfigurationChanges(configuration_changes) def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel": material_manager = self._application.getMaterialManager() diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3PrinterOutputController.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/ClusterUM3PrinterOutputController.py rename to plugins/UM3NetworkPrinting/src/Network/ClusterUM3PrinterOutputController.py diff --git a/plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py b/plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py new file mode 100644 index 0000000000..3a5bfb2651 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py @@ -0,0 +1,13 @@ +from typing import Optional, Callable + + +## Represents a request for adding a manual printer. It has the following fields: +# - address: The string of the (IP) address of the manual printer +# - callback: (Optional) Once the HTTP request to the printer to get printer information is done, whether successful +# or not, this callback will be invoked to notify about the result. The callback must have a signature of +# func(success: bool, address: str) -> None +class ManualPrinterRequest: + + def __init__(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: + self.address = address + self.callback = callback diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py b/plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py new file mode 100644 index 0000000000..1a72e7ff70 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py @@ -0,0 +1,23 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + + +## The network API client is responsible for handling requests and responses to printer over the local network (LAN). +class NetworkApiClient: + + API_PREFIX = "/cluster-api/v1/" + + def __init__(self) -> None: + pass + + def getPrinters(self): + pass + + def getPrintJobs(self): + pass + + def requestPrint(self): + pass + + def doPrintJobAction(self): + pass diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py new file mode 100644 index 0000000000..e74bdfd7a3 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -0,0 +1,425 @@ +from queue import Queue +from threading import Thread, Event +from time import time +from typing import Dict, Optional, Callable, List + +from PyQt5.QtCore import QUrl +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo + +from UM.Logger import Logger +from UM.Signal import Signal +from UM.Version import Version + +from cura.CuraApplication import CuraApplication +from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice +from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice +from plugins.UM3NetworkPrinting.src.Network.ManualPrinterRequest import ManualPrinterRequest + + +## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. +class NetworkOutputDeviceManager: + + PRINTER_API_VERSION = "1" + PRINTER_API_PREFIX = "/api/v" + PRINTER_API_VERSION + + CLUSTER_API_VERSION = "1" + CLUSTER_API_PREFIX = "/cluster-api/v" + CLUSTER_API_VERSION + + ZERO_CONF_NAME = u"_ultimaker._tcp.local." + + MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" + + MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0") + + discoveredDevicesChanged = Signal() + addedNetworkCluster = Signal() + removedNetworkCluster = Signal() + + def __init__(self): + + # Persistent dict containing the networked clusters. + self._discovered_devices = {} # type: Dict[str, ClusterUM3OutputDevice] + self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() + self._network_manager = QNetworkAccessManager() + # In order to avoid garbage collection we keep the callbacks in this list. + self._anti_gc_callbacks = [] # type: List[Callable[[], None]] + + self._zero_conf = None # type: Optional[Zeroconf] + self._zero_conf_browser = None # type: Optional[ServiceBrowser] + self._service_changed_request_queue = None # type: Optional[Queue] + self._service_changed_request_event = None # type: Optional[Event] + self._service_changed_request_thread = None # type: Optional[Thread] + + # Persistent dict containing manually connected clusters. + self._manual_instances = {} # type: Dict[str, ManualPrinterRequest] + self._last_manual_entry_key = None # type: Optional[str] + + # Hook up the signals for discovery. + self.addedNetworkCluster.connect(self._onAddDevice) + self.removedNetworkCluster.connect(self._onRemoveDevice) + + # # Get all discovered devices in the local network. + # def getDiscoveredDevices(self) -> Dict[str, ClusterUM3OutputDevice]: + # return self._discovered_devices + + # ## Get the key of the last manually added device. + # def getLastManualDevice(self) -> str: + # return self._last_manual_entry_key + + # ## Reset the last manually added device key. + # def resetLastManualDevice(self) -> None: + # self._last_manual_entry_key = "" + + ## Force reset all network device connections. + def refreshConnections(self): + active_machine = CuraApplication.getInstance().getGlobalContainerStack() + if not active_machine: + return + + um_network_key = active_machine.getMetaDataEntry("um_network_key") + + for key in self._discovered_devices: + if key == um_network_key: + if not self._discovered_devices[key].isConnected(): + Logger.log("d", "Attempting to connect with [%s]" % key) + # It should already be set, but if it actually connects we know for sure it's supported! + active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value) + self._discovered_devices[key].connect() + self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged) + else: + self._onDeviceConnectionStateChanged(key) + else: + if self._discovered_devices[key].isConnected(): + Logger.log("d", "Attempting to close connection with [%s]" % key) + self._discovered_devices[key].close() + self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + + ## Start the network discovery. + def start(self): + # The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. + # We can also re-schedule the requests when they fail to get detailed service info. + # Any new or re-reschedule requests will be appended to the request queue and the thread will process them. + self._service_changed_request_queue = Queue() + self._service_changed_request_event = Event() + self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True) + self._service_changed_request_thread.start() + + # Start network discovery. + self.stop() + self._zero_conf = Zeroconf() + self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [ + self._appendServiceChangedRequest + ]) + + # Load all manual devices. + self._manual_instances = self._getStoredManualInstances() + for address in self._manual_instances: + if address: + self.addManualDevice(address) + # TODO: self.resetLastManualDevice() + + ## Stop network discovery and clean up discovered devices. + def stop(self): + # Cleanup ZeroConf resources. + if self._zero_conf is not None: + self._zero_conf.close() + self._zero_conf = None + if self._zero_conf_browser is not None: + self._zero_conf_browser.cancel() + self._zero_conf_browser = None + + # Cleanup all manual devices. + for instance_name in list(self._discovered_devices): + self._onRemoveDevice(instance_name) + + ## Add a networked printer manually by address. + def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: + self._manual_instances[address] = ManualPrinterRequest(address, callback=callback) + new_manual_devices = ",".join(self._manual_instances.keys()) + CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) + + key = f"manual:{address}" + if key not in self._discovered_devices: + self._onAddDevice(key, address, { + b"name": address.encode("utf-8"), + b"address": address.encode("utf-8"), + b"manual": b"true", + b"incomplete": b"true", + b"temporary": b"true" + }) + + self._last_manual_entry_key = key + response_callback = lambda status_code, response: self._onCheckManualDeviceResponse(status_code, address) + self._checkManualDevice(address, response_callback) + + ## Remove a manually added networked printer. + def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: + if key not in self._discovered_devices and address is not None: + key = f"manual:{address}" + + if key in self._discovered_devices: + if not address: + address = self._discovered_devices[key].ipAddress + self._onRemoveDevice(key) + # TODO: self.resetLastManualDevice() + + if address in self._manual_instances: + manual_printer_request = self._manual_instances.pop(address) + new_manual_devices = ",".join(self._manual_instances.keys()) + CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, + new_manual_devices) + if manual_printer_request.callback is not None: + CuraApplication.getInstance().callLater(manual_printer_request.callback, False, address) + + ## Checks if a networked printer exists at the given address. + # If the printer responds it will replace the preliminary printer created from the stored manual instances. + def _checkManualDevice(self, address: str, on_finished: Callable) -> None: + Logger.log("d", "checking manual device: {}".format(address)) + url = QUrl(f"http://{address}/{self.PRINTER_API_PREFIX}/system") + request = QNetworkRequest(url) + reply = self._network_manager.get(request) + self._addCallback(reply, on_finished) + + ## Callback for when a manual device check request was responded to. + def _onCheckManualDeviceResponse(self, status_code: int, address: str) -> None: + Logger.log("d", "manual device check response: {} {}".format(status_code, address)) + if address in self._manual_instances: + callback = self._manual_instances[address].callback + if callback: + CuraApplication.getInstance().callLater(callback, status_code == 200, address) + + ## Returns a dict of printer BOM numbers to machine types. + # These numbers are available in the machine definition already so we just search for them here. + @staticmethod + def _getPrinterTypeIdentifiers() -> Dict[str, str]: + container_registry = CuraApplication.getInstance().getContainerRegistry() + ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.") + found_machine_type_identifiers = {} # type: Dict[str, str] + for machine in ultimaker_machines: + machine_bom_number = machine.get("firmware_update_info", {}).get("id", None) + machine_type = machine.get("id", None) + if machine_bom_number and machine_type: + found_machine_type_identifiers[str(machine_bom_number)] = machine_type + return found_machine_type_identifiers + + ## Add a new device. + def _onAddDevice(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: + cluster_size = int(properties.get(b"cluster_size", -1)) + printer_type = properties.get(b"machine", b"").decode("utf-8") + printer_type_identifiers = self._getPrinterTypeIdentifiers() + + # Detect the machine type based on the BOM number that is sent over the network. + for bom, p_type in printer_type_identifiers.items(): + if printer_type.startswith(bom): + properties[b"printer_type"] = bytes(p_type, encoding="utf8") + break + else: + properties[b"printer_type"] = b"Unknown" + + # We no longer support legacy devices, so check that here. + if cluster_size == -1: + return + + device = ClusterUM3OutputDevice(key, address, properties) + + CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( + ip_address=address, + key=device.getId(), + name=properties[b"name"].decode("utf-8"), + create_callback=self._createMachineFromDiscoveredPrinter, + machine_type=properties[b"printer_type"].decode("utf-8"), + device=device + ) + + self._discovered_devices[device.getId()] = device + self.discoveredDevicesChanged.emit() + + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() + if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"): + # Ensure that the configured connection type is set. + global_container_stack.addConfiguredConnectionType(device.connectionType.value) + device.connect() + device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged) + + ## Remove a device. + def _onRemoveDevice(self, device_id: str) -> None: + device = self._discovered_devices.pop(device_id, None) + if device: + if device.isConnected(): + device.disconnect() + try: + device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + except TypeError: + # Disconnect already happened. + pass + CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) + self.discoveredDevicesChanged.emit() + + ## Appends a service changed request so later the handling thread will pick it up and processes it. + def _appendServiceChangedRequest(self, zeroconf: Zeroconf, service_type, name: str, + state_change: ServiceStateChange) -> None: + item = (zeroconf, service_type, name, state_change) + self._service_changed_request_queue.put(item) + self._service_changed_request_event.set() + + def _handleOnServiceChangedRequests(self) -> None: + while True: + # Wait for the event to be set + self._service_changed_request_event.wait(timeout=5.0) + + # Stop if the application is shutting down + if CuraApplication.getInstance().isShuttingDown(): + return + + self._service_changed_request_event.clear() + + # Handle all pending requests + reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled + while not self._service_changed_request_queue.empty(): + request = self._service_changed_request_queue.get() + zeroconf, service_type, name, state_change = request + try: + result = self._onServiceChanged(zeroconf, service_type, name, state_change) + if not result: + reschedule_requests.append(request) + except Exception: + Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled", + service_type, name) + reschedule_requests.append(request) + + # Re-schedule the failed requests if any + if reschedule_requests: + for request in reschedule_requests: + self._service_changed_request_queue.put(request) + + ## Callback handler for when the connection state of a networked device has changed. + def _onDeviceConnectionStateChanged(self, key: str) -> None: + if key not in self._discovered_devices: + return + + if self._discovered_devices[key].isConnected(): + um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key") + if key != um_network_key: + return + CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(self._discovered_devices[key]) + # TODO: self.checkCloudFlowIsPossible(None) + else: + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(key) + + ## Handler for zeroConf detection. + # Return True or False indicating if the process succeeded. + # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. + def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange + ) -> bool: + if state_change == ServiceStateChange.Added: + return self._onServiceAdded(zero_conf, service_type, name) + elif state_change == ServiceStateChange.Removed: + return self._onServiceRemoved(name) + return True + + ## Handler for when a ZeroConf service was added. + def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: + # First try getting info from zero-conf cache + info = ServiceInfo(service_type, name, properties={}) + for record in zero_conf.cache.entries_with_name(name.lower()): + info.update_record(zero_conf, time(), record) + + for record in zero_conf.cache.entries_with_name(info.server): + info.update_record(zero_conf, time(), record) + if info.address: + break + + # Request more data if info is not complete + if not info.address: + info = zero_conf.get_service_info(service_type, name) + + if info: + type_of_device = info.properties.get(b"type", None) + if type_of_device: + if type_of_device == b"printer": + address = '.'.join(map(lambda n: str(n), info.address)) + self.addedNetworkCluster.emit(str(name), address, info.properties) + else: + Logger.log("w", + "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) + else: + Logger.log("w", "Could not get information about %s" % name) + return False + + return True + + ## Handler for when a ZeroConf service was removed. + def _onServiceRemoved(self, name: str) -> bool: + Logger.log("d", "Bonjour service removed: %s" % name) + self.removedNetworkCluster.emit(str(name)) + return True + + def _associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None: + if not printer_device: + return + + Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key) + + machine_manager = CuraApplication.getInstance().getMachineManager() + global_container_stack = machine_manager.activeMachine + if not global_container_stack: + return + + for machine in machine_manager.getMachinesInGroup(global_container_stack.getMetaDataEntry("group_id")): + machine.setMetaDataEntry("um_network_key", printer_device.key) + machine.setMetaDataEntry("group_name", printer_device.name) + + # Delete old authentication data. + Logger.log("d", "Removing old authentication id %s for device %s", + global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key) + + machine.removeMetaDataEntry("network_authentication_id") + machine.removeMetaDataEntry("network_authentication_key") + + # Ensure that these containers do know that they are configured for network connection + machine.addConfiguredConnectionType(printer_device.connectionType.value) + + self.refreshConnections() + + ## Create a machine instance based on the discovered network printer. + def _createMachineFromDiscoveredPrinter(self, key: str) -> None: + discovered_device = self._discovered_devices.get(key) + if discovered_device is None: + Logger.log("e", "Could not find discovered device with key [%s]", key) + return + + group_name = discovered_device.getProperty("name") + machine_type_id = discovered_device.getProperty("printer_type") + Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]", + key, group_name, machine_type_id) + + CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) + # connect the new machine to that network printer + self._associateActiveMachineWithPrinterDevice(discovered_device) + # ensure that the connection states are refreshed. + self.refreshConnections() + + ## Creates a callback function so that it includes the parsing of the response into the correct model. + # The callback is added to the 'finished' signal of the reply. + # \param reply: The reply that should be listened to. + # \param on_finished: The callback in case the response is successful. + def _addCallback(self, reply: QNetworkReply, on_finished: Callable) -> None: + def parse() -> None: + # Don't try to parse the reply if we didn't get one + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: + return + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + response = bytes(reply.readAll()).decode() + self._anti_gc_callbacks.remove(parse) + on_finished(int(status_code), response) + return + self._anti_gc_callbacks.append(parse) + reply.finished.connect(parse) + + ## Load the user-configured manual devices from Cura preferences. + def _getStoredManualInstances(self) -> Dict[str, ManualPrinterRequest]: + preferences = CuraApplication.getInstance().getPreferences() + preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") + manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") + return {address: ManualPrinterRequest(address) for address in manual_instances} diff --git a/plugins/UM3NetworkPrinting/src/Network/__init__.py b/plugins/UM3NetworkPrinting/src/Network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index f0fde818c4..8509b3aab3 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -9,12 +9,11 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from UM.Job import Job from UM.Logger import Logger from cura.CuraApplication import CuraApplication - -# Absolute imports don't work in plugins -from .Models import ClusterMaterial, LocalMaterial +from plugins.UM3NetworkPrinting.src.Models.ClusterMaterial import ClusterMaterial +from plugins.UM3NetworkPrinting.src.Models.LocalMaterial import LocalMaterial if TYPE_CHECKING: - from .ClusterUM3OutputDevice import ClusterUM3OutputDevice + from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice ## Asynchronous job to send material profiles to the printer. diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index f1607334eb..05c5ad1590 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -1,657 +1,226 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import json -import os -from queue import Queue -from threading import Event, Thread -from time import time -from typing import Optional, TYPE_CHECKING, Dict, Callable, Union, Any - -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo - -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QDesktopServices +from typing import Optional, TYPE_CHECKING, Callable from cura.CuraApplication import CuraApplication -from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from UM.i18n import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin -from UM.PluginRegistry import PluginRegistry -from UM.Signal import Signal, signalemitter -from UM.Version import Version +from plugins.UM3NetworkPrinting.src.Network.NetworkOutputDeviceManager import NetworkOutputDeviceManager -from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager -from .Cloud.CloudOutputDevice import CloudOutputDevice # typing if TYPE_CHECKING: - from PyQt5.QtNetwork import QNetworkReply from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin - from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice - from cura.Settings.GlobalStack import GlobalStack -i18n_catalog = i18nCatalog("cura") - - -# -# Represents a request for adding a manual printer. It has the following fields: -# - address: The string of the (IP) address of the manual printer -# - callback: (Optional) Once the HTTP request to the printer to get printer information is done, whether successful -# or not, this callback will be invoked to notify about the result. The callback must have a signature of -# func(success: bool, address: str) -> None -# - network_reply: This is the QNetworkReply instance for this request if the request has been issued and still in -# progress. It is kept here so we can cancel a request when needed. -# -class ManualPrinterRequest: - def __init__(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: - self.address = address - self.callback = callback - self.network_reply = None # type: Optional["QNetworkReply"] - - -## 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. -# If we discover a printer that has the same key as the active machine instance a connection is made. -@signalemitter +## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing. class UM3OutputDevicePlugin(OutputDevicePlugin): - addDeviceSignal = Signal() # Called '...Signal' to avoid confusion with function-names. - removeDeviceSignal = Signal() # Ditto ^^^. - discoveredDevicesChanged = Signal() - cloudFlowIsPossible = Signal() - def __init__(self): + # cloudFlowIsPossible = Signal() + + def __init__(self) -> None: super().__init__() - self._zero_conf = None - self._zero_conf_browser = None - - self._application = CuraApplication.getInstance() + # Create a network output device manager that abstracts all network connection logic away. + self._network_output_device_manager = NetworkOutputDeviceManager() # Create a cloud output device manager that abstracts all cloud connection logic away. self._cloud_output_device_manager = CloudOutputDeviceManager() - # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. - self.addDeviceSignal.connect(self._onAddDevice) - self.removeDeviceSignal.connect(self._onRemoveDevice) + # Refresh network connections when another machine was selected in Cura. + CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) - self._application.globalContainerStackChanged.connect(self.refreshConnections) - - self._discovered_devices = {} - - self._network_manager = QNetworkAccessManager() - self._network_manager.finished.connect(self._onNetworkRequestFinished) - - self._min_cluster_version = Version("4.0.0") - self._min_cloud_version = Version("5.2.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 = CuraApplication.getInstance().getPreferences() - self._preferences.addPreference("um3networkprinting/manual_instances", - "") # A comma-separated list of ip adresses or hostnames - - manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",") - self._manual_instances = {address: ManualPrinterRequest(address) - for address in manual_instances} # type: Dict[str, ManualPrinterRequest] - - # Store the last manual entry key - self._last_manual_entry_key = "" # type: str - - # 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. - # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick - # them up and process them. - self._service_changed_request_queue = Queue() - self._service_changed_request_event = Event() - self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True) - self._service_changed_request_thread.start() - - self._account = self._application.getCuraAPI().account + # TODO: re-write cloud messaging + # self._account = self._application.getCuraAPI().account # Check if cloud flow is possible when user logs in - self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible) + # self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible) # Check if cloud flow is possible when user switches machines - self._application.globalContainerStackChanged.connect(self._onMachineSwitched) + # self._application.globalContainerStackChanged.connect(self._onMachineSwitched) # Listen for when cloud flow is possible - self.cloudFlowIsPossible.connect(self._onCloudFlowPossible) + # self.cloudFlowIsPossible.connect(self._onCloudFlowPossible) - # Listen if cloud cluster was added - self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured) + # self._start_cloud_flow_message = None # type: Optional[Message] + # self._cloud_flow_complete_message = None # type: Optional[Message] - # Listen if cloud cluster was removed - self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible) + # self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured) + # self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible) - self._start_cloud_flow_message = None # type: Optional[Message] - self._cloud_flow_complete_message = None # type: Optional[Message] - - def getDiscoveredDevices(self): - return self._discovered_devices - - def getLastManualDevice(self) -> str: - return self._last_manual_entry_key - - def resetLastManualDevice(self) -> None: - self._last_manual_entry_key = "" - - ## Start looking for devices on network. + ## Start looking for devices in the network and cloud. def start(self): - self.startDiscovery() + self._network_output_device_manager.start() self._cloud_output_device_manager.start() - def startDiscovery(self): - self.stop() - if self._zero_conf_browser: - self._zero_conf_browser.cancel() - self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed. - - for instance_name in list(self._discovered_devices): - self._onRemoveDevice(instance_name) - - self._zero_conf = Zeroconf() - 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) - self.resetLastManualDevice() - - # TODO: CHANGE TO HOSTNAME - def refreshConnections(self): - active_machine = CuraApplication.getInstance().getGlobalContainerStack() - if not active_machine: - return - - um_network_key = active_machine.getMetaDataEntry("um_network_key") - - for key in self._discovered_devices: - if key == um_network_key: - if not self._discovered_devices[key].isConnected(): - Logger.log("d", "Attempting to connect with [%s]" % key) - # It should already be set, but if it actually connects we know for sure it's supported! - active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value) - self._discovered_devices[key].connect() - self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged) - else: - self._onDeviceConnectionStateChanged(key) - else: - if self._discovered_devices[key].isConnected(): - Logger.log("d", "Attempting to close connection with [%s]" % key) - self._discovered_devices[key].close() - self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) - - def _onDeviceConnectionStateChanged(self, key): - if key not in self._discovered_devices: - return - if self._discovered_devices[key].isConnected(): - # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine - um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key") - if key == um_network_key: - self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key]) - self.checkCloudFlowIsPossible(None) - else: - self.getOutputDeviceManager().removeOutputDevice(key) - - def stop(self): - if self._zero_conf is not None: - Logger.log("d", "zeroconf close...") - self._zero_conf.close() + # Stop network and cloud discovery. + def stop(self) -> None: + self._network_output_device_manager.stop() self._cloud_output_device_manager.stop() + ## Force refreshing the network connections. + def refreshConnections(self) -> None: + self._network_output_device_manager.refreshConnections() + self._cloud_output_device_manager.refreshConnections() + + ## Indicate that this plugin supports adding networked printers manually. def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt: - # This plugin should always be the fallback option (at least try it): return ManualDeviceAdditionAttempt.POSSIBLE - def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: - if key not in self._discovered_devices and address is not None: - key = "manual:%s" % address - - if key in self._discovered_devices: - if not address: - address = self._discovered_devices[key].ipAddress - self._onRemoveDevice(key) - self.resetLastManualDevice() - - if address in self._manual_instances: - manual_printer_request = self._manual_instances.pop(address) - self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances.keys())) - - if manual_printer_request.network_reply is not None: - manual_printer_request.network_reply.abort() - - if manual_printer_request.callback is not None: - self._application.callLater(manual_printer_request.callback, False, address) - + ## Add a networked printer manually based on its network address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: - self._manual_instances[address] = ManualPrinterRequest(address, callback = callback) - self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances.keys())) - - 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", - b"temporary": b"true" # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished - } - - if instance_name not in self._discovered_devices: - # Add a preliminary printer instance - self._onAddDevice(instance_name, address, properties) - self._last_manual_entry_key = instance_name - - reply = self._checkManualDevice(address) - self._manual_instances[address].network_reply = reply - - def _createMachineFromDiscoveredPrinter(self, key: str) -> None: - discovered_device = self._discovered_devices.get(key) - if discovered_device is None: - Logger.log("e", "Could not find discovered device with key [%s]", key) - return - - group_name = discovered_device.getProperty("name") - machine_type_id = discovered_device.getProperty("printer_type") - - Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]", - key, group_name, machine_type_id) - - self._application.getMachineManager().addMachine(machine_type_id, group_name) - # connect the new machine to that network printer - self.associateActiveMachineWithPrinterDevice(discovered_device) - # ensure that the connection states are refreshed. - self.refreshConnections() - - def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None: - if not printer_device: - return - - Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key) - - machine_manager = CuraApplication.getInstance().getMachineManager() - global_container_stack = machine_manager.activeMachine - if not global_container_stack: - return - - for machine in machine_manager.getMachinesInGroup(global_container_stack.getMetaDataEntry("group_id")): - machine.setMetaDataEntry("um_network_key", printer_device.key) - machine.setMetaDataEntry("group_name", printer_device.name) - - # Delete old authentication data. - Logger.log("d", "Removing old authentication id %s for device %s", - global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key) - - machine.removeMetaDataEntry("network_authentication_id") - machine.removeMetaDataEntry("network_authentication_key") - - # Ensure that these containers do know that they are configured for network connection - machine.addConfiguredConnectionType(printer_device.connectionType.value) - - self.refreshConnections() - - def _checkManualDevice(self, address: str) -> "QNetworkReply": - # 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) - return self._network_manager.get(name_request) - - def _onNetworkRequestFinished(self, reply: "QNetworkReply") -> None: - reply_url = reply.url().toString() - - address = reply.url().host() - device = None - properties = {} # type: Dict[bytes, bytes] - - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - # Either: - # - Something went wrong with checking the firmware version! - # - Something went wrong with checking the amount of printers the cluster has! - # - Couldn't find printer at the address when trying to add it manually. - if address in self._manual_instances: - key = "manual:" + address - self.removeManualDevice(key, address) - return - - if "system" in reply_url: - try: - system_info = json.loads(bytes(reply.readAll()).decode("utf-8")) - except: - Logger.log("e", "Something went wrong converting the JSON.") - return - - if address in self._manual_instances: - manual_printer_request = self._manual_instances[address] - manual_printer_request.network_reply = None - if manual_printer_request.callback is not None: - self._application.callLater(manual_printer_request.callback, True, address) - - has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version - instance_name = "manual:%s" % address - properties = { - b"name": (system_info["name"] + " (manual)").encode("utf-8"), - b"address": address.encode("utf-8"), - b"firmware_version": system_info["firmware"].encode("utf-8"), - b"manual": b"true", - b"machine": str(system_info['hardware']["typeid"]).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: - # 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 - instance_name = "manual:%s" % address - if instance_name in self._discovered_devices: - device = self._discovered_devices[instance_name] - properties = device.getProperties().copy() - if b"incomplete" in properties: - del properties[b"incomplete"] - properties[b"cluster_size"] = str(len(cluster_printers_list)).encode("utf-8") - self._onRemoveDevice(instance_name) - self._onAddDevice(instance_name, address, properties) - - def _onRemoveDevice(self, device_id: str) -> None: - device = self._discovered_devices.pop(device_id, None) - if device: - if device.isConnected(): - device.disconnect() - try: - device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) - except TypeError: - # Disconnect already happened. - pass - self._application.getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) - self.discoveredDevicesChanged.emit() - - ## Returns a dict of printer BOM numbers to machine types. - # These numbers are available in the machine definition already so we just search for them here. - def _getPrinterTypeIdentifiers(self) -> Dict[str, str]: - container_registry = self._application.getContainerRegistry() - ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.") - - found_machine_type_identifiers = {} # type: Dict[str, str] - for machine in ultimaker_machines: - machine_bom_number = machine.get("firmware_update_info", {}).get("id", None) - machine_type = machine.get("id", None) - if machine_bom_number and machine_type: - found_machine_type_identifiers[str(machine_bom_number)] = machine_type - - return found_machine_type_identifiers - - def _onAddDevice(self, name, address, properties): - # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster" - # or "Legacy" UM3 device. - cluster_size = int(properties.get(b"cluster_size", -1)) - - printer_type = properties.get(b"machine", b"").decode("utf-8") - printer_type_identifiers = self._getPrinterTypeIdentifiers() - - for key, value in printer_type_identifiers.items(): - if printer_type.startswith(key): - properties[b"printer_type"] = bytes(value, encoding="utf8") - break - else: - properties[b"printer_type"] = b"Unknown" - if cluster_size >= 0: - device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) - else: - device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) - self._application.getDiscoveredPrintersModel().addDiscoveredPrinter( - address, device.getId(), properties[b"name"].decode("utf-8"), self._createMachineFromDiscoveredPrinter, - properties[b"printer_type"].decode("utf-8"), device) - self._discovered_devices[device.getId()] = device - self.discoveredDevicesChanged.emit() - - global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() - if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"): - # Ensure that the configured connection type is set. - global_container_stack.addConfiguredConnectionType(device.connectionType.value) - device.connect() - device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged) - - ## Appends a service changed request so later the handling thread will pick it up and processes it. - def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change): - # append the request and set the event so the event handling thread can pick it up - item = (zeroconf, service_type, name, state_change) - self._service_changed_request_queue.put(item) - self._service_changed_request_event.set() - - def _handleOnServiceChangedRequests(self): - while True: - # Wait for the event to be set - self._service_changed_request_event.wait(timeout = 5.0) - - # Stop if the application is shutting down - if CuraApplication.getInstance().isShuttingDown(): - return - - self._service_changed_request_event.clear() - - # Handle all pending requests - reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled - while not self._service_changed_request_queue.empty(): - request = self._service_changed_request_queue.get() - zeroconf, service_type, name, state_change = request - try: - result = self._onServiceChanged(zeroconf, service_type, name, state_change) - if not result: - reschedule_requests.append(request) - except Exception: - Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled", - service_type, name) - reschedule_requests.append(request) - - # Re-schedule the failed requests if any - if reschedule_requests: - for request in reschedule_requests: - self._service_changed_request_queue.put(request) - - ## Handler for zeroConf detection. - # Return True or False indicating if the process succeeded. - # Note that this function can take over 3 seconds to complete. Be careful - # calling it from the main thread. - def _onServiceChanged(self, zero_conf, service_type, name, state_change): - if state_change == ServiceStateChange.Added: - # First try getting info from zero-conf cache - info = ServiceInfo(service_type, name, properties = {}) - for record in zero_conf.cache.entries_with_name(name.lower()): - info.update_record(zero_conf, time(), record) - - for record in zero_conf.cache.entries_with_name(info.server): - info.update_record(zero_conf, time(), record) - if info.address: - break - - # Request more data if info is not complete - if not info.address: - info = zero_conf.get_service_info(service_type, name) - - if info: - type_of_device = info.properties.get(b"type", None) - if type_of_device: - if type_of_device == b"printer": - address = '.'.join(map(lambda n: str(n), info.address)) - self.addDeviceSignal.emit(str(name), address, info.properties) - else: - Logger.log("w", - "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) - else: - Logger.log("w", "Could not get information about %s" % name) - return False - - elif state_change == ServiceStateChange.Removed: - Logger.log("d", "Bonjour service removed: %s" % name) - self.removeDeviceSignal.emit(str(name)) - - return True - - ## Check if the prerequsites are in place to start the cloud flow - def checkCloudFlowIsPossible(self, cluster: Optional[CloudOutputDevice]) -> None: - Logger.log("d", "Checking if cloud connection is possible...") - - # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again - active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack] - if active_machine: - # Check 1A: Printer isn't already configured for cloud - if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes: - Logger.log("d", "Active machine was already configured for cloud.") - return - - # Check 1B: Printer isn't already configured for cloud - if active_machine.getMetaDataEntry("cloud_flow_complete", False): - Logger.log("d", "Active machine was already configured for cloud.") - return - - # Check 2: User did not already say "Don't ask me again" - if active_machine.getMetaDataEntry("do_not_show_cloud_message", False): - Logger.log("d", "Active machine shouldn't ask about cloud anymore.") - return - - # Check 3: User is logged in with an Ultimaker account - if not self._account.isLoggedIn: - Logger.log("d", "Cloud Flow not possible: User not logged in!") - return - - # Check 4: Machine is configured for network connectivity - if not self._application.getMachineManager().activeMachineHasNetworkConnection: - Logger.log("d", "Cloud Flow not possible: Machine is not connected!") - return - - # Check 5: Machine has correct firmware version - firmware_version = self._application.getMachineManager().activeMachineFirmwareVersion # type: str - if not Version(firmware_version) > self._min_cloud_version: - Logger.log("d", "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)", - firmware_version, - self._min_cloud_version) - return - - Logger.log("d", "Cloud flow is possible!") - self.cloudFlowIsPossible.emit() - - def _onCloudFlowPossible(self) -> None: - # Cloud flow is possible, so show the message - if not self._start_cloud_flow_message: - self._createCloudFlowStartMessage() - if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible: - self._start_cloud_flow_message.show() - - def _onCloudPrintingConfigured(self, device) -> None: - # Hide the cloud flow start message if it was hanging around already - # For example: if the user already had the browser openen and made the association themselves - if self._start_cloud_flow_message and self._start_cloud_flow_message.visible: - self._start_cloud_flow_message.hide() - - # Cloud flow is complete, so show the message - if not self._cloud_flow_complete_message: - self._createCloudFlowCompleteMessage() - if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible: - self._cloud_flow_complete_message.show() - - # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers - active_machine = self._application.getMachineManager().activeMachine - if active_machine: - - # The active machine _might_ not be the machine that was in the added cloud cluster and - # then this will hide the cloud message for the wrong machine. So we only set it if the - # host names match between the active machine and the newly added cluster - saved_host_name = active_machine.getMetaDataEntry("um_network_key", "").split('.')[0] - added_host_name = device.toDict()["host_name"] - - if added_host_name == saved_host_name: - active_machine.setMetaDataEntry("do_not_show_cloud_message", True) - - return - - def _onDontAskMeAgain(self, checked: bool) -> None: - active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack] - if active_machine: - active_machine.setMetaDataEntry("do_not_show_cloud_message", checked) - if checked: - Logger.log("d", "Will not ask the user again to cloud connect for current printer.") - return - - def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None: - address = self._application.getMachineManager().activeMachineAddress # type: str - if address: - QDesktopServices.openUrl(QUrl("http://" + address + "/cloud_connect")) - if self._start_cloud_flow_message: - self._start_cloud_flow_message.hide() - self._start_cloud_flow_message = None - return - - def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None: - address = self._application.getMachineManager().activeMachineAddress # type: str - if address: - QDesktopServices.openUrl(QUrl("http://" + address + "/settings")) - return - - def _onMachineSwitched(self) -> None: - # Hide any left over messages - if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible: - self._start_cloud_flow_message.hide() - if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible: - self._cloud_flow_complete_message.hide() - - # Check for cloud flow again with newly selected machine - self.checkCloudFlowIsPossible(None) - - def _createCloudFlowStartMessage(self): - self._start_cloud_flow_message = Message( - text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), - lifetime = 0, - image_source = QUrl.fromLocalFile(os.path.join( - PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - "resources", "svg", "cloud-flow-start.svg" - )), - image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"), - option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), - option_state = False - ) - self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "") - self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain) - self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted) - - def _createCloudFlowCompleteMessage(self): - self._cloud_flow_complete_message = Message( - text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), - lifetime = 30, - image_source = QUrl.fromLocalFile(os.path.join( - PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - "resources", "svg", "cloud-flow-completed.svg" - )), - image_caption = i18n_catalog.i18nc("@info:status", "Connected!") - ) - self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon - self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection) + self._network_output_device_manager.addManualDevice(address, callback) + + ## Remove a manually connected networked printer. + def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: + self._network_output_device_manager.removeManualDevice(key, address) + + # ## Get the last manual device attempt. + # # Used by the DiscoverUM3Action. + # def getLastManualDevice(self) -> str: + # return self._network_output_device_manager.getLastManualDevice() + + # ## Reset the last manual device attempt. + # # Used by the DiscoverUM3Action. + # def resetLastManualDevice(self) -> None: + # self._network_output_device_manager.resetLastManualDevice() + + # ## Check if the prerequsites are in place to start the cloud flow + # def checkCloudFlowIsPossible(self, cluster: Optional[CloudOutputDevice]) -> None: + # Logger.log("d", "Checking if cloud connection is possible...") + # + # # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again + # active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack] + # if active_machine: + # # Check 1A: Printer isn't already configured for cloud + # if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes: + # Logger.log("d", "Active machine was already configured for cloud.") + # return + # + # # Check 1B: Printer isn't already configured for cloud + # if active_machine.getMetaDataEntry("cloud_flow_complete", False): + # Logger.log("d", "Active machine was already configured for cloud.") + # return + # + # # Check 2: User did not already say "Don't ask me again" + # if active_machine.getMetaDataEntry("do_not_show_cloud_message", False): + # Logger.log("d", "Active machine shouldn't ask about cloud anymore.") + # return + # + # # Check 3: User is logged in with an Ultimaker account + # if not self._account.isLoggedIn: + # Logger.log("d", "Cloud Flow not possible: User not logged in!") + # return + # + # # Check 4: Machine is configured for network connectivity + # if not self._application.getMachineManager().activeMachineHasNetworkConnection: + # Logger.log("d", "Cloud Flow not possible: Machine is not connected!") + # return + # + # # Check 5: Machine has correct firmware version + # firmware_version = self._application.getMachineManager().activeMachineFirmwareVersion # type: str + # if not Version(firmware_version) > self._min_cloud_version: + # Logger.log("d", "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)", + # firmware_version, + # self._min_cloud_version) + # return + # + # Logger.log("d", "Cloud flow is possible!") + # self.cloudFlowIsPossible.emit() + + # def _onCloudFlowPossible(self) -> None: + # # Cloud flow is possible, so show the message + # if not self._start_cloud_flow_message: + # self._createCloudFlowStartMessage() + # if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible: + # self._start_cloud_flow_message.show() + + # def _onCloudPrintingConfigured(self, device) -> None: + # # Hide the cloud flow start message if it was hanging around already + # # For example: if the user already had the browser openen and made the association themselves + # if self._start_cloud_flow_message and self._start_cloud_flow_message.visible: + # self._start_cloud_flow_message.hide() + # + # # Cloud flow is complete, so show the message + # if not self._cloud_flow_complete_message: + # self._createCloudFlowCompleteMessage() + # if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible: + # self._cloud_flow_complete_message.show() + # + # # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers + # active_machine = self._application.getMachineManager().activeMachine + # if active_machine: + # + # # The active machine _might_ not be the machine that was in the added cloud cluster and + # # then this will hide the cloud message for the wrong machine. So we only set it if the + # # host names match between the active machine and the newly added cluster + # saved_host_name = active_machine.getMetaDataEntry("um_network_key", "").split('.')[0] + # added_host_name = device.toDict()["host_name"] + # + # if added_host_name == saved_host_name: + # active_machine.setMetaDataEntry("do_not_show_cloud_message", True) + # + # return + + # def _onDontAskMeAgain(self, checked: bool) -> None: + # active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack] + # if active_machine: + # active_machine.setMetaDataEntry("do_not_show_cloud_message", checked) + # if checked: + # Logger.log("d", "Will not ask the user again to cloud connect for current printer.") + # return + + # def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None: + # address = self._application.getMachineManager().activeMachineAddress # type: str + # if address: + # QDesktopServices.openUrl(QUrl("http://" + address + "/cloud_connect")) + # if self._start_cloud_flow_message: + # self._start_cloud_flow_message.hide() + # self._start_cloud_flow_message = None + # return + + # def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None: + # address = self._application.getMachineManager().activeMachineAddress # type: str + # if address: + # QDesktopServices.openUrl(QUrl("http://" + address + "/settings")) + # return + + # def _onMachineSwitched(self) -> None: + # # Hide any left over messages + # if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible: + # self._start_cloud_flow_message.hide() + # if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible: + # self._cloud_flow_complete_message.hide() + # + # # Check for cloud flow again with newly selected machine + # self.checkCloudFlowIsPossible(None) + + # def _createCloudFlowStartMessage(self): + # self._start_cloud_flow_message = Message( + # text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), + # lifetime = 0, + # image_source = QUrl.fromLocalFile(os.path.join( + # PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), + # "resources", "svg", "cloud-flow-start.svg" + # )), + # image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"), + # option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), + # option_state = False + # ) + # self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "") + # self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain) + # self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted) + + # def _createCloudFlowCompleteMessage(self): + # self._cloud_flow_complete_message = Message( + # text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), + # lifetime = 30, + # image_source = QUrl.fromLocalFile(os.path.join( + # PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), + # "resources", "svg", "cloud-flow-completed.svg" + # )), + # image_caption = i18n_catalog.i18nc("@info:status", "Connected!") + # ) + # self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon + # self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py new file mode 100644 index 0000000000..8f311a52f1 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -0,0 +1,102 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import List, Optional, Dict + +from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot + +from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState +from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel + + +## Output device class that forms the basis of Ultimaker networked printer output devices. +# Currently used for local networking and cloud printing using Ultimaker Connect. +# This base class primarily contains all the Qt properties and slots needed for the monitor page to work. +class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): + + # Signal emitted when the status of the print jobs for this cluster were changed over the network. + printJobsChanged = pyqtSignal() + + # Signal emitted when the currently visible printer card in the UI was changed by the user. + activePrinterChanged = pyqtSignal() + + # Notify can only use signals that are defined by the class that they are in, not inherited ones. + # Therefore we create a private signal used to trigger the printersChanged signal. + _clusterPrintersChanged = pyqtSignal() + + # States indicating if a print job is queued. + QUEUED_PRINT_JOBS_STATES = {"queued", "error"} + + def __init__(self, device_id, address, properties, connection_type, parent=None) -> None: + super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, + parent=parent) + + # Keeps track of all print jobs in the cluster. + self._print_jobs = [] # type: List[UM3PrintJobOutputModel] + + # Keep track of the printer currently selected in the UI. + self._active_printer = None # type: Optional[PrinterOutputModel] + + # By default we are not authenticated. This state will be changed later. + self._authentication_state = AuthState.NotAuthenticated + + # Get all print jobs for this cluster. + @pyqtProperty("QVariantList", notify=printJobsChanged) + def printJobs(self) -> List[UM3PrintJobOutputModel]: + return self._print_jobs + + # Get all print jobs for this cluster that are queued. + @pyqtProperty("QVariantList", notify=printJobsChanged) + def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]: + return [print_job for print_job in self._print_jobs if print_job.state in self.QUEUED_PRINT_JOBS_STATES] + + # Get all print jobs for this cluster that are currently printing. + @pyqtProperty("QVariantList", notify=printJobsChanged) + def activePrintJobs(self) -> List[UM3PrintJobOutputModel]: + return [print_job for print_job in self._print_jobs if + print_job.assignedPrinter is not None and print_job.state not in self.QUEUED_PRINT_JOBS_STATES] + + # Get the amount of printers in the cluster. + @pyqtProperty(int, notify=_clusterPrintersChanged) + def clusterSize(self) -> int: + return self._cluster_size + + # Get the amount of printer in the cluster per type. + @pyqtProperty("QVariantList", notify=_clusterPrintersChanged) + def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: + printer_count = {} # type: Dict[str, int] + for printer in self._printers: + if printer.type in printer_count: + printer_count[printer.type] += 1 + else: + printer_count[printer.type] = 1 + result = [] + for machine_type in printer_count: + result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])}) + return result + + # Get a list of all printers. + @pyqtProperty("QVariantList", notify=_clusterPrintersChanged) + def printers(self) -> List[PrinterOutputModel]: + return self._printers + + # Get the currently active printer in the UI. + @pyqtProperty(QObject, notify=activePrinterChanged) + def activePrinter(self) -> Optional[PrinterOutputModel]: + return self._active_printer + + # Set the currently active printer from the UI. + @pyqtSlot(QObject, name="setActivePrinter") + def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: + if self.activePrinter == printer: + return + self._active_printer = printer + self.activePrinterChanged.emit() + + @pyqtSlot(name="openPrintJobControlPanel") + def openPrintJobControlPanel(self) -> None: + raise NotImplementedError("openPrintJobControlPanel must be implemented") + + @pyqtSlot(name="openPrinterControlPanel") + def openPrinterControlPanel(self) -> None: + raise NotImplementedError("openPrinterControlPanel must be implemented") diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py index b79d009c31..c1de91cf7c 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py @@ -7,11 +7,11 @@ from unittest.mock import patch, MagicMock from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot from ...src.Cloud import CloudApiClient -from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse -from ...src.Cloud.Models.CloudClusterStatus import CloudClusterStatus -from ...src.Cloud.Models.CloudPrintJobResponse import CloudPrintJobResponse -from ...src.Cloud.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from ...src.Cloud.Models.CloudError import CloudError +from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse +from plugins.UM3NetworkPrinting.src.Models.CloudClusterStatus import CloudClusterStatus +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse +from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest +from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError from .Fixtures import readFixture, parseFixture from .NetworkManagerMock import NetworkManagerMock @@ -60,7 +60,7 @@ class TestCloudApiClient(TestCase): self.assertEqual([CloudClusterStatus(**data)], result) def test_requestUpload(self): - + results = [] response = readFixture("putJobUploadResponse") @@ -74,7 +74,7 @@ class TestCloudApiClient(TestCase): self.assertEqual(["uploading"], [r.status for r in results]) def test_uploadToolPath(self): - + results = [] progress = MagicMock() @@ -94,7 +94,7 @@ class TestCloudApiClient(TestCase): self.assertEqual(["sent"], results) def test_requestPrint(self): - + results = [] response = readFixture("postJobPrintResponse") diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py index 352efb292e..46a2414005 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py @@ -9,7 +9,7 @@ from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from ...src.Cloud import CloudApiClient from ...src.Cloud.CloudOutputDevice import CloudOutputDevice -from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse +from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse from .Fixtures import readFixture, parseFixture from .NetworkManagerMock import NetworkManagerMock @@ -46,7 +46,7 @@ class TestCloudOutputDevice(TestCase): self.onError = MagicMock() with patch.object(CloudApiClient, "QNetworkAccessManager", return_value = self.network): self._api = CloudApiClient.CloudApiClient(self.account, self.onError) - + self.device = CloudOutputDevice(self._api, self.cluster) self.cluster_status = parseFixture("getClusterStatusResponse") self.network.prepareReply("GET", self.STATUS_URL, 200, readFixture("getClusterStatusResponse")) @@ -138,7 +138,7 @@ class TestCloudOutputDevice(TestCase): }, { "extension": "gcode.gz", "mime_type": "application/gzip", - "mode": 2, + "mode": 2, }] file_handler.getWriterByMimeType.return_value.write.side_effect = \ lambda stream, nodes: stream.write(str(nodes).encode()) diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py index 869b39440c..b0d1c83f8d 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py @@ -7,7 +7,7 @@ from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot from ...src.Cloud import CloudApiClient from ...src.Cloud import CloudOutputDeviceManager -from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse +from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse from .Fixtures import parseFixture, readFixture from .NetworkManagerMock import NetworkManagerMock, FakeSignal diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From fb434ec81a8dddbf103c89046829526c4066f5d6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 26 Jul 2019 15:08:49 +0200 Subject: [PATCH 02/52] Remove unneeded import --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 05c5ad1590..7f7f69a241 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -1,6 +1,6 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, TYPE_CHECKING, Callable +from typing import Optional, Callable from cura.CuraApplication import CuraApplication @@ -10,9 +10,6 @@ from plugins.UM3NetworkPrinting.src.Network.NetworkOutputDeviceManager import Ne from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager -if TYPE_CHECKING: - from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin - ## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing. class UM3OutputDevicePlugin(OutputDevicePlugin): From bd4f0c4e25a386adb619fa9f07dcf59eef3dd7fb Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 26 Jul 2019 15:34:37 +0200 Subject: [PATCH 03/52] Extract API calls to separate class --- .../src/Cloud/CloudOutputDeviceManager.py | 1 + .../src/Network/ClusterApiClient.py | 61 +++++++++++++++ .../src/Network/NetworkOutputDeviceManager.py | 78 +++++++------------ 3 files changed, 88 insertions(+), 52 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 55b6af8214..1cc109666f 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -24,6 +24,7 @@ from .Utils import findChanges # API spec is available on https://api.ultimaker.com/docs/connect/spec/. # class CloudOutputDeviceManager: + META_CLUSTER_ID = "um_cloud_cluster_id" # The interval with which the remote clusters are checked diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py new file mode 100644 index 0000000000..183829bb50 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -0,0 +1,61 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import Callable, List, Optional + +from PyQt5.QtCore import QUrl +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply + + +## The ClusterApiClient is responsible for all network calls to local network clusters. +class ClusterApiClient: + + PRINTER_API_VERSION = "1" + PRINTER_API_PREFIX = "/api/v" + PRINTER_API_VERSION + + CLUSTER_API_VERSION = "1" + CLUSTER_API_PREFIX = "/cluster-api/v" + CLUSTER_API_VERSION + + ## Initializes a new cluster API client. + # \param address: The network address of the cluster to call. + # \param on_error: The callback to be called whenever we receive errors from the server. + def __init__(self, address: str, on_error: Callable) -> None: + super().__init__() + self._manager = QNetworkAccessManager() + self._address = address + self._on_error = on_error + self._upload = None # type: # Optional[ToolPathUploader] + # In order to avoid garbage collection we keep the callbacks in this list. + self._anti_gc_callbacks = [] # type: List[Callable[[], None]] + + ## Get printer system information. + # \param on_finished: The callback in case the response is successful. + def getSystem(self, on_finished: Callable) -> None: + url = f"{self.PRINTER_API_PREFIX}/system" + reply = self._manager.get(self._createEmptyRequest(url)) + self._addCallback(reply, on_finished) + + ## We override _createEmptyRequest in order to add the user credentials. + # \param url: The URL to request + # \param content_type: The type of the body contents. + def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: + request = QNetworkRequest(QUrl(self._address + path)) + if content_type: + request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) + return request + + ## Creates a callback function so that it includes the parsing of the response into the correct model. + # The callback is added to the 'finished' signal of the reply. + # \param reply: The reply that should be listened to. + # \param on_finished: The callback in case the response is successful. + def _addCallback(self, reply: QNetworkReply, on_finished: Callable) -> None: + def parse() -> None: + # Don't try to parse the reply if we didn't get one + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: + return + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + response = bytes(reply.readAll()).decode() + self._anti_gc_callbacks.remove(parse) + on_finished(int(status_code), response) + return + self._anti_gc_callbacks.append(parse) + reply.finished.connect(parse) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index e74bdfd7a3..613ad1a0d6 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -1,18 +1,21 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from queue import Queue from threading import Thread, Event from time import time -from typing import Dict, Optional, Callable, List +from typing import Dict, Optional, Callable -from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo +from UM import i18nCatalog from UM.Logger import Logger +from UM.Message import Message from UM.Signal import Signal from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice +from plugins.UM3NetworkPrinting.src.Network.ClusterApiClient import ClusterApiClient from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice from plugins.UM3NetworkPrinting.src.Network.ManualPrinterRequest import ManualPrinterRequest @@ -20,30 +23,22 @@ from plugins.UM3NetworkPrinting.src.Network.ManualPrinterRequest import ManualPr ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. class NetworkOutputDeviceManager: - PRINTER_API_VERSION = "1" - PRINTER_API_PREFIX = "/api/v" + PRINTER_API_VERSION - - CLUSTER_API_VERSION = "1" - CLUSTER_API_PREFIX = "/cluster-api/v" + CLUSTER_API_VERSION - ZERO_CONF_NAME = u"_ultimaker._tcp.local." - MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" - MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0") + # The translation catalog for this device. + I18N_CATALOG = i18nCatalog("cura") + discoveredDevicesChanged = Signal() addedNetworkCluster = Signal() removedNetworkCluster = Signal() - def __init__(self): + def __init__(self) -> None: # Persistent dict containing the networked clusters. self._discovered_devices = {} # type: Dict[str, ClusterUM3OutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() - self._network_manager = QNetworkAccessManager() - # In order to avoid garbage collection we keep the callbacks in this list. - self._anti_gc_callbacks = [] # type: List[Callable[[], None]] self._zero_conf = None # type: Optional[Zeroconf] self._zero_conf_browser = None # type: Optional[ServiceBrowser] @@ -59,18 +54,6 @@ class NetworkOutputDeviceManager: self.addedNetworkCluster.connect(self._onAddDevice) self.removedNetworkCluster.connect(self._onRemoveDevice) - # # Get all discovered devices in the local network. - # def getDiscoveredDevices(self) -> Dict[str, ClusterUM3OutputDevice]: - # return self._discovered_devices - - # ## Get the key of the last manually added device. - # def getLastManualDevice(self) -> str: - # return self._last_manual_entry_key - - # ## Reset the last manually added device key. - # def resetLastManualDevice(self) -> None: - # self._last_manual_entry_key = "" - ## Force reset all network device connections. def refreshConnections(self): active_machine = CuraApplication.getInstance().getGlobalContainerStack() @@ -93,7 +76,8 @@ class NetworkOutputDeviceManager: if self._discovered_devices[key].isConnected(): Logger.log("d", "Attempting to close connection with [%s]" % key) self._discovered_devices[key].close() - self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + self._discovered_devices[key].connectionStateChanged.disconnect( + self._onDeviceConnectionStateChanged) ## Start the network discovery. def start(self): @@ -117,7 +101,6 @@ class NetworkOutputDeviceManager: for address in self._manual_instances: if address: self.addManualDevice(address) - # TODO: self.resetLastManualDevice() ## Stop network discovery and clean up discovered devices. def stop(self): @@ -172,14 +155,22 @@ class NetworkOutputDeviceManager: if manual_printer_request.callback is not None: CuraApplication.getInstance().callLater(manual_printer_request.callback, False, address) + ## Handles an API error received from the cloud. + # \param errors: The errors received + def _onApiError(self, errors) -> None: + Logger.log("w", str(errors)) + message = Message( + text=self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the printer."), + title=self.I18N_CATALOG.i18nc("@info:title", "Error"), + lifetime=10 + ) + message.show() + ## Checks if a networked printer exists at the given address. # If the printer responds it will replace the preliminary printer created from the stored manual instances. def _checkManualDevice(self, address: str, on_finished: Callable) -> None: - Logger.log("d", "checking manual device: {}".format(address)) - url = QUrl(f"http://{address}/{self.PRINTER_API_PREFIX}/system") - request = QNetworkRequest(url) - reply = self._network_manager.get(request) - self._addCallback(reply, on_finished) + api_client = ClusterApiClient(address, self._onApiError) + api_client.getSystem(on_finished) ## Callback for when a manual device check request was responded to. def _onCheckManualDeviceResponse(self, status_code: int, address: str) -> None: @@ -355,7 +346,7 @@ class NetworkOutputDeviceManager: self.removedNetworkCluster.emit(str(name)) return True - def _associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None: + def _associateActiveMachineWithPrinterDevice(self, printer_device: Optional[PrinterOutputDevice]) -> None: if not printer_device: return @@ -400,23 +391,6 @@ class NetworkOutputDeviceManager: # ensure that the connection states are refreshed. self.refreshConnections() - ## Creates a callback function so that it includes the parsing of the response into the correct model. - # The callback is added to the 'finished' signal of the reply. - # \param reply: The reply that should be listened to. - # \param on_finished: The callback in case the response is successful. - def _addCallback(self, reply: QNetworkReply, on_finished: Callable) -> None: - def parse() -> None: - # Don't try to parse the reply if we didn't get one - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: - return - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - response = bytes(reply.readAll()).decode() - self._anti_gc_callbacks.remove(parse) - on_finished(int(status_code), response) - return - self._anti_gc_callbacks.append(parse) - reply.finished.connect(parse) - ## Load the user-configured manual devices from Cura preferences. def _getStoredManualInstances(self) -> Dict[str, ManualPrinterRequest]: preferences = CuraApplication.getInstance().getPreferences() From 4268c011a761b3e45b5c98e24cb14ddcbf441ebe Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 26 Jul 2019 17:05:39 +0200 Subject: [PATCH 04/52] Start inheriting both output devices from a base device --- .../src/Cloud/CloudOutputDevice.py | 103 ++---------------- .../src/Cloud/CloudOutputDeviceManager.py | 2 +- .../src/Network/ClusterUM3OutputDevice.py | 60 +++------- .../UltimakerNetworkedPrinterOutputDevice.py | 54 ++++++++- .../src/{Cloud => }/Utils.py | 0 5 files changed, 79 insertions(+), 140 deletions(-) rename plugins/UM3NetworkPrinting/src/{Cloud => }/Utils.py (100%) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 237f961acf..b9ca1e05ec 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -3,7 +3,7 @@ import os from time import time -from typing import Dict, List, Optional, Set, cast +from typing import List, Optional, Set, cast from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtGui import QDesktopServices @@ -14,14 +14,13 @@ from UM.FileHandler.FileHandler import FileHandler from UM.Logger import Logger from UM.Message import Message from UM.PluginRegistry import PluginRegistry -from UM.Qt.Duration import Duration, DurationFormat from UM.Scene.SceneNode import SceneNode from UM.Version import Version from cura.CuraApplication import CuraApplication -from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice -from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from plugins.UM3NetworkPrinting.src.UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice from .CloudOutputController import CloudOutputController from ..MeshFormatHandler import MeshFormatHandler @@ -35,7 +34,7 @@ from plugins.UM3NetworkPrinting.src.Models.CloudPrintResponse import CloudPrintR from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse from plugins.UM3NetworkPrinting.src.Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus from plugins.UM3NetworkPrinting.src.Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus -from .Utils import formatDateCompleted, formatTimeCompleted + I18N_CATALOG = i18nCatalog("cura") @@ -44,7 +43,8 @@ I18N_CATALOG = i18nCatalog("cura") # Currently it only supports viewing the printer and print job status and adding a new job to the queue. # As such, those methods have been implemented here. # Note that this device represents a single remote cluster, not a list of multiple clusters. -class CloudOutputDevice(NetworkedPrinterOutputDevice): +class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): + # The interval with which the remote clusters are checked CHECK_CLUSTER_INTERVAL = 10.0 # seconds @@ -81,12 +81,11 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): super().__init__(device_id=cluster.cluster_id, address="", connection_type=ConnectionType.CloudConnection, properties=properties, parent=parent) self._api = api_client + self._account = api_client.account self._cluster = cluster self._setInterfaceElements() - self._account = api_client.account - # We use the Cura Connect monitor tab to get most functionality right away. if PluginRegistry.getInstance() is not None: plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") @@ -98,13 +97,6 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) - # We keep track of which printer is visible in the monitor page. - self._active_printer = None # type: Optional[PrinterOutputModel] - - # Properties to populate later on with received cloud data. - self._print_jobs = [] # type: List[UM3PrintJobOutputModel] - self._number_of_extruders = 2 # All networked printers are dual-extrusion Ultimaker machines. - # We only allow a single upload at a time. self._progress = CloudProgressMessage() @@ -382,98 +374,27 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): firmware_version = Version([version_number[0], version_number[1], version_number[2]]) return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION - ## 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]: - return self._printers - - ## Get the active printer in the UI (monitor page). - @pyqtProperty(QObject, notify=activePrinterChanged) - def activePrinter(self) -> Optional[PrinterOutputModel]: - return self._active_printer - - ## Set the active printer in the UI (monitor page). - @pyqtSlot(QObject) - def setActivePrinter(self, printer: Optional[PrinterOutputModel] = None) -> None: - if printer != self._active_printer: - self._active_printer = printer - self.activePrinterChanged.emit() - - ## Get remote print jobs. - @pyqtProperty("QVariantList", notify=printJobsChanged) - def printJobs(self) -> List[UM3PrintJobOutputModel]: - return self._print_jobs - - ## Get remote print jobs that are still in the print queue. - @pyqtProperty("QVariantList", notify=printJobsChanged) - def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]: - return [print_job for print_job in self._print_jobs - if print_job.state == "queued" or print_job.state == "error"] - - ## Get remote print jobs that are assigned to a printer. - @pyqtProperty("QVariantList", notify=printJobsChanged) - def activePrintJobs(self) -> List[UM3PrintJobOutputModel]: - return [print_job for print_job in self._print_jobs if - print_job.assignedPrinter is not None and print_job.state != "queued"] - ## Set the remote print job state. def setJobState(self, print_job_uuid: str, state: str) -> None: self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state) - @pyqtSlot(str) + @pyqtSlot(str, name="sendJobToTop") def sendJobToTop(self, print_job_uuid: str) -> None: self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "move", {"list": "queued", "to_position": 0}) - @pyqtSlot(str) + @pyqtSlot(str, name="deleteJobFromQueue") def deleteJobFromQueue(self, print_job_uuid: str) -> None: self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "remove") - @pyqtSlot(str) + @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "force") - @pyqtSlot(int, result=str) - def formatDuration(self, seconds: int) -> str: - return Duration(seconds).getDisplayString(DurationFormat.Format.Short) - - @pyqtSlot(int, result=str) - def getTimeCompleted(self, time_remaining: int) -> str: - return formatTimeCompleted(time_remaining) - - @pyqtSlot(int, result=str) - def getDateCompleted(self, time_remaining: int) -> str: - return formatDateCompleted(time_remaining) - - @pyqtProperty(bool, notify=printJobsChanged) - def receivedPrintJobs(self) -> bool: - return bool(self._print_jobs) - - @pyqtSlot() + @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com")) - @pyqtSlot() + @pyqtSlot(name="openPrinterControlPanel") def openPrinterControlPanel(self) -> None: QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com")) - - ## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud. - # TODO: We fake the methods here to not break the monitor page. - - @pyqtProperty(QUrl, notify=_clusterPrintersChanged) - def activeCameraUrl(self) -> "QUrl": - return QUrl() - - @pyqtSlot(QUrl) - def setActiveCameraUrl(self, camera_url: "QUrl") -> None: - pass - - @pyqtProperty("QVariantList", notify=_clusterPrintersChanged) - def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: - return [] diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 1cc109666f..847665a754 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -15,7 +15,7 @@ from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError -from .Utils import findChanges +from plugins.UM3NetworkPrinting.src.Utils import findChanges ## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py index f39c4b2d9b..898a2f6ae2 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py @@ -1,6 +1,5 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - from typing import Any, cast, Tuple, Union, Optional, Dict, List from time import time @@ -14,7 +13,6 @@ from UM.i18n import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.PluginRegistry import PluginRegistry -from UM.Qt.Duration import Duration, DurationFormat from UM.Scene.SceneNode import SceneNode # For typing. from UM.Settings.ContainerRegistry import ContainerRegistry @@ -27,7 +25,6 @@ from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from plugins.UM3NetworkPrinting.src.Factories.PrinterModelFactory import PrinterModelFactory from plugins.UM3NetworkPrinting.src.UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice -from plugins.UM3NetworkPrinting.src.Cloud.Utils import formatTimeCompleted, formatDateCompleted from plugins.UM3NetworkPrinting.src.Network.ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController from plugins.UM3NetworkPrinting.src.MeshFormatHandler import MeshFormatHandler from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob @@ -43,7 +40,6 @@ i18n_catalog = i18nCatalog("cura") class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): activeCameraUrlChanged = pyqtSignal() - receivedPrintJobsChanged = pyqtSignal() def __init__(self, device_id, address, properties, parent = None) -> None: super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent) @@ -57,8 +53,6 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): "", {}, io.BytesIO() ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]] - self._received_print_jobs = False # type: bool - if PluginRegistry.getInstance() is not None: plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") if plugin_path is None: @@ -113,10 +107,9 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): if len(self._printers) > 1: # We need to ask the user. self._spawnPrinterSelectionDialog() - is_job_sent = True else: # Just immediately continue. self._sending_job.send("") # No specifically selected printer. - is_job_sent = self._sending_job.send(None) + self._sending_job.send(None) def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: @@ -129,11 +122,6 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): if self._printer_selection_dialog is not None: self._printer_selection_dialog.show() - ## Whether the printer that this output device represents supports print job actions via the local network. - @pyqtProperty(bool, constant=True) - def supportsPrintJobActions(self) -> bool: - return True - ## Allows the user to choose a printer to print with from the printer # selection dialogue. # \param target_printer The name of the printer to target. @@ -225,27 +213,27 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress) - @pyqtProperty(QUrl, notify = activeCameraUrlChanged) - def activeCameraUrl(self) -> "QUrl": + @pyqtProperty(QUrl, notify=activeCameraUrlChanged) + def activeCameraUrl(self) -> QUrl: return self._active_camera_url - @pyqtSlot(QUrl) - def setActiveCameraUrl(self, camera_url: "QUrl") -> None: + @pyqtSlot(QUrl, name="setActiveCameraUrl") + def setActiveCameraUrl(self, camera_url: QUrl) -> None: if self._active_camera_url != camera_url: self._active_camera_url = camera_url self.activeCameraUrlChanged.emit() + ## The IP address of the printer. + @pyqtProperty(str, constant = True) + def address(self) -> str: + return self._address + def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: if self._progress_message: self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - ## The IP address of the printer. - @pyqtProperty(str, constant = True) - def address(self) -> str: - return self._address - def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 @@ -294,44 +282,26 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: - Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs")) @pyqtSlot(name="openPrinterControlPanel") def openPrinterControlPanel(self) -> None: - Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) - @pyqtProperty(bool, notify = receivedPrintJobsChanged) - def receivedPrintJobs(self) -> bool: - return self._received_print_jobs - - @pyqtSlot(int, result = str) - def getTimeCompleted(self, time_remaining: int) -> str: - return formatTimeCompleted(time_remaining) - - @pyqtSlot(int, result = str) - def getDateCompleted(self, time_remaining: int) -> str: - return formatDateCompleted(time_remaining) - - @pyqtSlot(int, result = str) - def formatDuration(self, seconds: int) -> str: - return Duration(seconds).getDisplayString(DurationFormat.Format.Short) - - @pyqtSlot(str) + @pyqtSlot(str, name="sendJobToTop") def sendJobToTop(self, print_job_uuid: str) -> None: # This function is part of the output device (and not of the printjob output model) as this type of operation # is a modification of the cluster queue and not of the actual job. data = "{\"to_position\": 0}" self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None) - @pyqtSlot(str) + @pyqtSlot(str, name="deleteJobFromQueue") def deleteJobFromQueue(self, print_job_uuid: str) -> None: # This function is part of the output device (and not of the printjob output model) as this type of operation # is a modification of the cluster queue and not of the actual job. self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None) - @pyqtSlot(str) + @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: data = "{\"force\": true}" self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None) @@ -392,9 +362,6 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: - self._received_print_jobs = True - self.receivedPrintJobsChanged.emit() - if not checkValidGetReply(reply): return @@ -634,6 +601,7 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): job = SendMaterialJob(device = self) job.run() + def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 8f311a52f1..f2a1df3a4d 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -2,10 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import List, Optional, Dict -from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot +from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot, QUrl +from UM.Qt.Duration import Duration, DurationFormat from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState +from plugins.UM3NetworkPrinting.src.Utils import formatTimeCompleted, formatDateCompleted from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel @@ -31,6 +33,9 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, parent=parent) + # Trigger the printersChanged signal when the private signal is triggered. + self.printersChanged.connect(self._clusterPrintersChanged) + # Keeps track of all print jobs in the cluster. self._print_jobs = [] # type: List[UM3PrintJobOutputModel] @@ -56,10 +61,14 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state not in self.QUEUED_PRINT_JOBS_STATES] + @pyqtProperty(bool, notify=printJobsChanged) + def receivedPrintJobs(self) -> bool: + return bool(self._print_jobs) + # Get the amount of printers in the cluster. @pyqtProperty(int, notify=_clusterPrintersChanged) def clusterSize(self) -> int: - return self._cluster_size + return max(1, len(self._printers)) # Get the amount of printer in the cluster per type. @pyqtProperty("QVariantList", notify=_clusterPrintersChanged) @@ -93,6 +102,27 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._active_printer = printer self.activePrinterChanged.emit() + ## Whether the printer that this output device represents supports print job actions via the local network. + @pyqtProperty(bool, constant=True) + def supportsPrintJobActions(self) -> bool: + return True + + ## Set the remote print job state. + def setJobState(self, print_job_uuid: str, state: str) -> None: + raise NotImplementedError("setJobState must be implemented") + + @pyqtSlot(str, name="sendJobToTop") + def sendJobToTop(self, print_job_uuid: str) -> None: + raise NotImplementedError("sendJobToTop must be implemented") + + @pyqtSlot(str, name="deleteJobFromQueue") + def deleteJobFromQueue(self, print_job_uuid: str) -> None: + raise NotImplementedError("deleteJobFromQueue must be implemented") + + @pyqtSlot(str, name="forceSendJob") + def forceSendJob(self, print_job_uuid: str) -> None: + raise NotImplementedError("forceSendJob must be implemented") + @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: raise NotImplementedError("openPrintJobControlPanel must be implemented") @@ -100,3 +130,23 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): @pyqtSlot(name="openPrinterControlPanel") def openPrinterControlPanel(self) -> None: raise NotImplementedError("openPrinterControlPanel must be implemented") + + @pyqtProperty(QUrl, notify=_clusterPrintersChanged) + def activeCameraUrl(self) -> QUrl: + return QUrl() + + @pyqtSlot(QUrl, name="setActiveCameraUrl") + def setActiveCameraUrl(self, camera_url: QUrl) -> None: + pass + + @pyqtSlot(int, result=str, name="getTimeCompleted") + def getTimeCompleted(self, time_remaining: int) -> str: + return formatTimeCompleted(time_remaining) + + @pyqtSlot(int, result=str, name="getDateCompleted") + def getDateCompleted(self, time_remaining: int) -> str: + return formatDateCompleted(time_remaining) + + @pyqtSlot(int, result=str, name="formatDuration") + def formatDuration(self, seconds: int) -> str: + return Duration(seconds).getDisplayString(DurationFormat.Format.Short) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Utils.py b/plugins/UM3NetworkPrinting/src/Utils.py similarity index 100% rename from plugins/UM3NetworkPrinting/src/Cloud/Utils.py rename to plugins/UM3NetworkPrinting/src/Utils.py From 4b212d6c05bbc9cd1c4205533b7996bf6fd83c99 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 14:53:50 +0200 Subject: [PATCH 05/52] Merge more stuff, re-use models for local networking as well --- .../resources/qml/MonitorQueue.qml | 12 +- .../resources/qml/MonitorStage.qml | 14 +- .../src/Cloud/CloudApiClient.py | 13 +- .../src/Cloud/CloudOutputDevice.py | 172 +--- .../src/Cloud/CloudOutputDeviceManager.py | 16 +- .../src/Cloud/ToolPathUploader.py | 3 +- ...ntroller.py => ClusterOutputController.py} | 15 +- .../src/Factories/PrinterModelFactory.py | 30 - .../src/Models/BaseCloudModel.py | 55 -- .../src/Models/BaseModel.py | 51 ++ .../Models/{ => Http}/CloudClusterResponse.py | 6 +- .../Models/{ => Http}/CloudClusterStatus.py | 18 +- .../src/Models/{ => Http}/CloudError.py | 6 +- .../{ => Http}/CloudPrintJobResponse.py | 6 +- .../{ => Http}/CloudPrintJobUploadRequest.py | 6 +- .../Models/{ => Http}/CloudPrintResponse.py | 6 +- .../ClusterBuildPlate.py} | 10 +- .../ClusterPrintCoreConfiguration.py} | 12 +- .../ClusterPrintJobConfigurationChange.py} | 6 +- .../ClusterPrintJobConstraint.py} | 6 +- .../ClusterPrintJobImpediment.py} | 9 +- .../ClusterPrintJobStatus.py} | 45 +- .../ClusterPrinterConfigurationMaterial.py} | 9 +- .../ClusterPrinterStatus.py} | 18 +- .../src/Models/LocalMaterial.py | 2 +- .../src/Network/ClusterApiClient.py | 92 +- .../src/Network/ClusterUM3OutputDevice.py | 862 ++++++------------ .../ClusterUM3PrinterOutputController.py | 20 - .../src/Network/NetworkApiClient.py | 23 - .../src/Network/NetworkOutputDeviceManager.py | 4 +- .../UltimakerNetworkedPrinterOutputDevice.py | 116 ++- 31 files changed, 688 insertions(+), 975 deletions(-) rename plugins/UM3NetworkPrinting/src/{Cloud/CloudOutputController.py => ClusterOutputController.py} (56%) delete mode 100644 plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py delete mode 100644 plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py rename plugins/UM3NetworkPrinting/src/Models/{ => Http}/CloudClusterResponse.py (93%) rename plugins/UM3NetworkPrinting/src/Models/{ => Http}/CloudClusterStatus.py (55%) rename plugins/UM3NetworkPrinting/src/Models/{ => Http}/CloudError.py (90%) rename plugins/UM3NetworkPrinting/src/Models/{ => Http}/CloudPrintJobResponse.py (92%) rename plugins/UM3NetworkPrinting/src/Models/{ => Http}/CloudPrintJobUploadRequest.py (81%) rename plugins/UM3NetworkPrinting/src/Models/{ => Http}/CloudPrintResponse.py (88%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterBuildPlate.py => Http/ClusterBuildPlate.py} (50%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrintCoreConfiguration.py => Http/ClusterPrintCoreConfiguration.py} (82%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrintJobConfigurationChange.py => Http/ClusterPrintJobConfigurationChange.py} (87%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrintJobConstraint.py => Http/ClusterPrintJobConstraint.py} (79%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrintJobImpediment.py => Http/ClusterPrintJobImpediment.py} (73%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrintJobStatus.py => Http/ClusterPrintJobStatus.py} (77%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrinterConfigurationMaterial.py => Http/ClusterPrinterConfigurationMaterial.py} (92%) rename plugins/UM3NetworkPrinting/src/Models/{CloudClusterPrinterStatus.py => Http/ClusterPrinterStatus.py} (84%) delete mode 100644 plugins/UM3NetworkPrinting/src/Network/ClusterUM3PrinterOutputController.py delete mode 100644 plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml index 6727c7bd8c..cc9fad5233 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml @@ -186,17 +186,7 @@ Item } printJob: modelData } - model: - { - // When printing over the cloud we don't recieve print jobs until there is one, so - // unless there's at least one print job we'll be stuck with skeleton loading - // indefinitely. - if (Cura.MachineManager.activeMachineIsUsingCloudConnection || OutputDevice.receivedPrintJobs) - { - return OutputDevice.queuedPrintJobs - } - return [null, null] - } + model: OutputDevice.queuedPrintJobs spacing: 6 // TODO: Theme! } } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml index e68418c21a..b92535a560 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml @@ -25,7 +25,7 @@ Component } width: maximumWidth color: UM.Theme.getColor("monitor_stage_background") - + // Enable keyboard navigation. NOTE: This is done here so that we can also potentially // forward to the queue items in the future. (Deleting selected print job, etc.) Keys.forwardTo: carousel @@ -50,17 +50,7 @@ Component MonitorCarousel { id: carousel - printers: - { - // When printing over the cloud we don't recieve print jobs until there is one, so - // unless there's at least one print job we'll be stuck with skeleton loading - // indefinitely. - if (Cura.MachineManager.activeMachineIsUsingCloudConnection || OutputDevice.receivedPrintJobs) - { - return OutputDevice.printers - } - return [null] - } + printers: OutputDevice.printers } } diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 9868e4a5d3..bd61b945cf 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -11,14 +11,15 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManage from UM.Logger import Logger from cura import UltimakerCloudAuthentication from cura.API import Account + from .ToolPathUploader import ToolPathUploader from ..Models.BaseModel import BaseModel -from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse -from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError -from plugins.UM3NetworkPrinting.src.Models.CloudClusterStatus import CloudClusterStatus -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from plugins.UM3NetworkPrinting.src.Models.CloudPrintResponse import CloudPrintResponse -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse +from ..Models.Http.CloudClusterResponse import CloudClusterResponse +from ..Models.Http.CloudError import CloudError +from ..Models.Http.CloudClusterStatus import CloudClusterStatus +from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest +from ..Models.Http.CloudPrintResponse import CloudPrintResponse +from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse ## The generic type variable used to document the methods below. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index b9ca1e05ec..b337da4ef5 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -1,7 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import os - from time import time from typing import List, Optional, Set, cast @@ -13,27 +11,24 @@ from UM.Backend.Backend import BackendState from UM.FileHandler.FileHandler import FileHandler from UM.Logger import Logger from UM.Message import Message -from UM.PluginRegistry import PluginRegistry from UM.Scene.SceneNode import SceneNode from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from plugins.UM3NetworkPrinting.src.UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice -from .CloudOutputController import CloudOutputController -from ..MeshFormatHandler import MeshFormatHandler -from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from .CloudProgressMessage import CloudProgressMessage from .CloudApiClient import CloudApiClient -from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse -from plugins.UM3NetworkPrinting.src.Models.CloudClusterStatus import CloudClusterStatus -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from plugins.UM3NetworkPrinting.src.Models.CloudPrintResponse import CloudPrintResponse -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse -from plugins.UM3NetworkPrinting.src.Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus -from plugins.UM3NetworkPrinting.src.Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus +from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice +from ..MeshFormatHandler import MeshFormatHandler +from ..Models.Http.CloudClusterResponse import CloudClusterResponse +from ..Models.Http.CloudClusterStatus import CloudClusterStatus +from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest +from ..Models.Http.CloudPrintResponse import CloudPrintResponse +from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse +from ..Models.Http.ClusterPrinterStatus import ClusterPrinterStatus +from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus I18N_CATALOG = i18nCatalog("cura") @@ -78,22 +73,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): b"cluster_size": b"1" # cloud devices are always clusters of at least one } - super().__init__(device_id=cluster.cluster_id, address="", - connection_type=ConnectionType.CloudConnection, properties=properties, parent=parent) + super().__init__( + device_id=cluster.cluster_id, + address="", + connection_type=ConnectionType.CloudConnection, + properties=properties, + parent=parent + ) + self._api = api_client self._account = api_client.account self._cluster = cluster + self.setAuthenticationState(AuthState.NotAuthenticated) self._setInterfaceElements() - # We use the Cura Connect monitor tab to get most functionality right away. - if PluginRegistry.getInstance() is not None: - plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") - if plugin_path is None: - Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting") - raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting") - self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml") - # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) @@ -101,11 +95,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._progress = CloudProgressMessage() # Keep server string of the last generated time to avoid updating models more than once for the same response - self._received_printers = None # type: Optional[List[CloudClusterPrinterStatus]] - self._received_print_jobs = None # type: Optional[List[CloudClusterPrintJobStatus]] - - # A set of the user's job IDs that have finished - self._finished_jobs = set() # type: Set[str] + self._received_printers = None # type: Optional[List[ClusterPrinterStatus]] + self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]] # Reference to the uploaded print job / mesh self._tool_path = None # type: Optional[bytes] @@ -130,33 +121,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._tool_path = None self._uploaded_print_job = None - ## Gets the cluster response from which this device was created. - @property - def clusterData(self) -> CloudClusterResponse: - return self._cluster - - ## Updates the cluster data from the cloud. - @clusterData.setter - def clusterData(self, value: CloudClusterResponse) -> None: - self._cluster = value - ## Checks whether the given network key is found in the cloud's host name def matchesNetworkKey(self, network_key: str) -> bool: # Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local." # the host name should then be "ultimakersystem-aabbccdd0011" if network_key.startswith(self.clusterData.host_name): return True - # However, for manually added printers, the local IP address is used in lieu of a proper # network key, so check for that as well if self.clusterData.host_internal_ip is not None and network_key.find(self.clusterData.host_internal_ip): return True - return False ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: - self.setPriority(2) # Make sure we end up below the local networking and above 'save to file' + self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'. self.setName(self._id) self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud")) @@ -227,105 +206,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) - ## Updates the local list of printers with the list received from the cloud. - # \param remote_printers: The printers received from the cloud. - def _updatePrinters(self, remote_printers: List[CloudClusterPrinterStatus]) -> None: - - # Keep track of the new printers to show. - # We create a new list instead of changing the existing one to get the correct order. - new_printers = [] - - # Check which printers need to be created or updated. - for index, printer_data in enumerate(remote_printers): - printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid), None) - if not printer: - new_printers.append(printer_data.createOutputModel(CloudOutputController(self))) - else: - printer_data.updateOutputModel(printer) - new_printers.append(printer) - - # Check which printers need to be removed (de-referenced). - remote_printers_keys = [printer_data.uuid for printer_data in remote_printers] - removed_printers = [printer for printer in self._printers if printer.key not in remote_printers_keys] - for removed_printer in removed_printers: - if self._active_printer and self._active_printer.key == removed_printer.key: - self.setActivePrinter(None) - - self._printers = new_printers - if self._printers and not self.activePrinter: - self.setActivePrinter(self._printers[0]) - - self.printersChanged.emit() - - ## Updates the local list of print jobs with the list received from the cloud. - # \param remote_jobs: The print jobs received from the cloud. - def _updatePrintJobs(self, remote_jobs: List[CloudClusterPrintJobStatus]) -> None: - - # Keep track of the new print jobs to show. - # We create a new list instead of changing the existing one to get the correct order. - new_print_jobs = [] - - # Check which print jobs need to be created or updated. - for index, print_job_data in enumerate(remote_jobs): - print_job = next( - iter(print_job for print_job in self._print_jobs if print_job.key == print_job_data.uuid), None) - if not print_job: - new_print_jobs.append(self._createPrintJobModel(print_job_data)) - else: - print_job_data.updateOutputModel(print_job) - if print_job_data.printer_uuid: - self._updateAssignedPrinter(print_job, print_job_data.printer_uuid) - new_print_jobs.append(print_job) - - # Check which print job need to be removed (de-referenced). - remote_job_keys = [print_job_data.uuid for print_job_data in remote_jobs] - removed_jobs = [print_job for print_job in self._print_jobs if print_job.key not in remote_job_keys] - for removed_job in removed_jobs: - if removed_job.assignedPrinter: - removed_job.assignedPrinter.updateActivePrintJob(None) - removed_job.stateChanged.disconnect(self._onPrintJobStateChanged) - - self._print_jobs = new_print_jobs - self.printJobsChanged.emit() - - ## Create a new print job model based on the remote status of the job. - # \param remote_job: The remote print job data. - def _createPrintJobModel(self, remote_job: CloudClusterPrintJobStatus) -> UM3PrintJobOutputModel: - model = remote_job.createOutputModel(CloudOutputController(self)) - model.stateChanged.connect(self._onPrintJobStateChanged) - if remote_job.printer_uuid: - self._updateAssignedPrinter(model, remote_job.printer_uuid) - return model - - ## Handles the event of a change in a print job state - def _onPrintJobStateChanged(self) -> None: - user_name = self._getUserName() - # TODO: confirm that notifications in Cura are still required - for job in self._print_jobs: - if job.state == "wait_cleanup" and job.key not in self._finished_jobs and job.owner == user_name: - self._finished_jobs.add(job.key) - Message( - title=I18N_CATALOG.i18nc("@info:status", "Print finished"), - text=(I18N_CATALOG.i18nc("@info:status", - "Printer '{printer_name}' has finished printing '{job_name}'.").format( - printer_name=job.assignedPrinter.name, - job_name=job.name - ) if job.assignedPrinter else - I18N_CATALOG.i18nc("@info:status", "The print job '{job_name}' was finished.").format( - job_name=job.name - )), - ).show() - - ## Updates the printer assignment for the given print job model. - def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: - printer = next((p for p in self._printers if printer_uuid == p.key), None) - if not printer: - Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key, - [p.key for p in self._printers]) - return - printer.updateActivePrintJob(model) - model.updateAssignedPrinter(printer) - ## Uploads the mesh when the print job was registered with the cloud API. # \param job_response: The response received from the cloud API. def _onPrintJobCreated(self, job_response: CloudPrintJobResponse) -> None: @@ -398,3 +278,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(name="openPrinterControlPanel") def openPrinterControlPanel(self) -> None: QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com")) + + ## Gets the cluster response from which this device was created. + @property + def clusterData(self) -> CloudClusterResponse: + return self._cluster + + ## Updates the cluster data from the cloud. + @clusterData.setter + def clusterData(self, value: CloudClusterResponse) -> None: + self._cluster = value diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 847665a754..e1e54c2991 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -11,11 +11,12 @@ from UM.Signal import Signal from cura.API import Account from cura.CuraApplication import CuraApplication from cura.Settings.GlobalStack import GlobalStack + from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice -from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse -from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError -from plugins.UM3NetworkPrinting.src.Utils import findChanges +from ..Models.Http.CloudClusterResponse import CloudClusterResponse +from ..Models.Http.CloudError import CloudError +from ..Utils import findChanges ## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. @@ -186,14 +187,9 @@ class CloudOutputDeviceManager: ## Handles an API error received from the cloud. # \param errors: The errors received - def _onApiError(self, errors: List[CloudError] = None) -> None: + @staticmethod + def _onApiError(errors: List[CloudError] = None) -> None: Logger.log("w", str(errors)) - message = Message( - text = self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the cloud."), - title = self.I18N_CATALOG.i18nc("@info:title", "Error"), - lifetime = 10 - ) - message.show() ## Starts running the cloud output device manager, thus periodically requesting cloud data. def start(self): diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 4faad4c6d8..55b41d1c1c 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -6,7 +6,8 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManage from typing import Optional, Callable, Any, Tuple, cast from UM.Logger import Logger -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse + +from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse ## Class responsible for uploading meshes to the cloud in separate requests. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputController.py b/plugins/UM3NetworkPrinting/src/ClusterOutputController.py similarity index 56% rename from plugins/UM3NetworkPrinting/src/Cloud/CloudOutputController.py rename to plugins/UM3NetworkPrinting/src/ClusterOutputController.py index 8c09483990..775297e2c0 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputController.py +++ b/plugins/UM3NetworkPrinting/src/ClusterOutputController.py @@ -2,18 +2,13 @@ # Cura is released under the terms of the LGPLv3 or higher. from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController - -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from .CloudOutputDevice import CloudOutputDevice +from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice -class CloudOutputController(PrinterOutputController): - def __init__(self, output_device: "CloudOutputDevice") -> None: +class ClusterOutputController(PrinterOutputController): + + def __init__(self, output_device: PrinterOutputDevice) -> None: super().__init__(output_device) - - # The cloud connection only supports fetching the printer and queue status and adding a job to the queue. - # To let the UI know this we mark all features below as False. self.can_pause = True self.can_abort = True self.can_pre_heat_bed = False @@ -22,5 +17,5 @@ class CloudOutputController(PrinterOutputController): self.can_control_manually = False self.can_update_firmware = False - def setJobState(self, job: "PrintJobOutputModel", state: str): + def setJobState(self, job: PrintJobOutputModel, state: str): self._output_device.setJobState(job.key, state) diff --git a/plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py b/plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py deleted file mode 100644 index df6903ec71..0000000000 --- a/plugins/UM3NetworkPrinting/src/Factories/PrinterModelFactory.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import List, Any, Dict - -from PyQt5.QtCore import QUrl - -from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel -from cura.PrinterOutput.PrinterOutputController import PrinterOutputController -from plugins.UM3NetworkPrinting.src.Models.ConfigurationChangeModel import ConfigurationChangeModel - - -class PrinterModelFactory: - - CAMERA_URL_FORMAT = "http://{ip_address}:8080/?action=stream" - - # Create a printer output model from some data. - @classmethod - def createPrinter(cls, output_controller: PrinterOutputController, ip_address: str, extruder_count: int = 2 - ) -> PrinterOutputModel: - printer = PrinterOutputModel(output_controller=output_controller, number_of_extruders=extruder_count) - printer.setCameraUrl(QUrl(cls.CAMERA_URL_FORMAT.format(ip_address=ip_address))) - return printer - - # Create a list of configuration change models. - @classmethod - def createConfigurationChanges(cls, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]: - return [ConfigurationChangeModel( - type_of_change=change.get("type_of_change"), - index=change.get("index"), - target_name=change.get("target_name"), - origin_name=change.get("origin_name") - ) for change in data] diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py deleted file mode 100644 index af7115d738..0000000000 --- a/plugins/UM3NetworkPrinting/src/Models/BaseCloudModel.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from datetime import datetime, timezone -from typing import Dict, Union, TypeVar, Type, List, Any - -from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel - - -## Base class for the models used in the interface with the Ultimaker cloud APIs. -class BaseCloudModel(BaseModel): - ## Checks whether the two models are equal. - # \param other: The other model. - # \return True if they are equal, False if they are different. - def __eq__(self, other): - return type(self) == type(other) and self.toDict() == other.toDict() - - ## Checks whether the two models are different. - # \param other: The other model. - # \return True if they are different, False if they are the same. - def __ne__(self, other) -> bool: - return type(self) != type(other) or self.toDict() != other.toDict() - - ## Converts the model into a serializable dictionary - def toDict(self) -> Dict[str, Any]: - return self.__dict__ - - # Type variable used in the parse methods below, which should be a subclass of BaseModel. - T = TypeVar("T", bound=BaseModel) - - ## Parses a single model. - # \param model_class: The model class. - # \param values: The value of the model, which is usually a dictionary, but may also be already parsed. - # \return An instance of the model_class given. - @staticmethod - def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T: - if isinstance(values, dict): - return model_class(**values) - return values - - ## Parses a list of models. - # \param model_class: The model class. - # \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. - # \return A list of instances of the model_class given. - @classmethod - def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]: - return [cls.parseModel(model_class, value) for value in values] - - ## Parses the given date string. - # \param date: The date to parse. - # \return The parsed date. - @staticmethod - def parseDate(date: Union[str, datetime]) -> datetime: - if isinstance(date, datetime): - return date - return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc) diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index a48a9f838e..2c5c667f89 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -1,5 +1,10 @@ ## Base model that maps kwargs to instance attributes. +from datetime import datetime, timezone +from typing import TypeVar, Dict, List, Any, Type, Union + + class BaseModel: + def __init__(self, **kwargs) -> None: self.__dict__.update(kwargs) self.validate() @@ -7,3 +12,49 @@ class BaseModel: # Validates the model, raising an exception if the model is invalid. def validate(self) -> None: pass + + ## Checks whether the two models are equal. + # \param other: The other model. + # \return True if they are equal, False if they are different. + def __eq__(self, other): + return type(self) == type(other) and self.toDict() == other.toDict() + + ## Checks whether the two models are different. + # \param other: The other model. + # \return True if they are different, False if they are the same. + def __ne__(self, other) -> bool: + return type(self) != type(other) or self.toDict() != other.toDict() + + ## Converts the model into a serializable dictionary + def toDict(self) -> Dict[str, Any]: + return self.__dict__ + + # Type variable used in the parse methods below, which should be a subclass of BaseModel. + T = TypeVar("T", bound="BaseModel") + + ## Parses a single model. + # \param model_class: The model class. + # \param values: The value of the model, which is usually a dictionary, but may also be already parsed. + # \return An instance of the model_class given. + @staticmethod + def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T: + if isinstance(values, dict): + return model_class(**values) + return values + + ## Parses a list of models. + # \param model_class: The model class. + # \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. + # \return A list of instances of the model_class given. + @classmethod + def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]: + return [cls.parseModel(model_class, value) for value in values] + + ## Parses the given date string. + # \param date: The date to parse. + # \return The parsed date. + @staticmethod + def parseDate(date: Union[str, datetime]) -> datetime: + if isinstance(date, datetime): + return date + return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc) diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py similarity index 93% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterResponse.py rename to plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index a872a6ba68..c2b87a9efb 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -2,12 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel ## Class representing a cloud connected cluster. -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterResponse(BaseCloudModel): +class CloudClusterResponse(BaseModel): + ## Creates a new cluster response object. # \param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. # \param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'. diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py similarity index 55% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterStatus.py rename to plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index b0250c2ebb..dbc5f24480 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -3,24 +3,24 @@ from datetime import datetime from typing import List, Dict, Union, Any -from .CloudClusterPrinterStatus import CloudClusterPrinterStatus -from .CloudClusterPrintJobStatus import CloudClusterPrintJobStatus -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel +from .ClusterPrinterStatus import ClusterPrinterStatus +from .ClusterPrintJobStatus import ClusterPrintJobStatus # Model that represents the status of the cluster for the cloud -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterStatus(BaseCloudModel): +class CloudClusterStatus(BaseModel): + ## Creates a new cluster status model object. # \param printers: The latest status of each printer in the cluster. # \param print_jobs: The latest status of each print job in the cluster. # \param generated_time: The datetime when the object was generated on the server-side. def __init__(self, - printers: List[Union[CloudClusterPrinterStatus, Dict[str, Any]]], - print_jobs: List[Union[CloudClusterPrintJobStatus, Dict[str, Any]]], + printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], + print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], generated_time: Union[str, datetime], **kwargs) -> None: self.generated_time = self.parseDate(generated_time) - self.printers = self.parseModels(CloudClusterPrinterStatus, printers) - self.print_jobs = self.parseModels(CloudClusterPrintJobStatus, print_jobs) + self.printers = self.parseModels(ClusterPrinterStatus, printers) + self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudError.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py similarity index 90% rename from plugins/UM3NetworkPrinting/src/Models/CloudError.py rename to plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py index b53361022e..4ba8f50293 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudError.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py @@ -2,12 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Dict, Optional, Any -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel ## Class representing errors generated by the cloud servers, according to the JSON-API standard. -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudError(BaseCloudModel): +class CloudError(BaseModel): + ## Creates a new error object. # \param id: Unique identifier for this particular occurrence of the problem. # \param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py similarity index 92% rename from plugins/UM3NetworkPrinting/src/Models/CloudPrintJobResponse.py rename to plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py index 79196ee38c..7c056fcb5e 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudPrintJobResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py @@ -2,12 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel # Model that represents the response received from the cloud after requesting to upload a print job -# Spec: https://api-staging.ultimaker.com/cura/v1/spec -class CloudPrintJobResponse(BaseCloudModel): +class CloudPrintJobResponse(BaseModel): + ## Creates a new print job response model. # \param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. # \param status: The status of the print job. diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py similarity index 81% rename from plugins/UM3NetworkPrinting/src/Models/CloudPrintJobUploadRequest.py rename to plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py index e59c571558..d221683a5b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudPrintJobUploadRequest.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py @@ -1,11 +1,11 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel # Model that represents the request to upload a print job to the cloud -# Spec: https://api-staging.ultimaker.com/cura/v1/spec -class CloudPrintJobUploadRequest(BaseCloudModel): +class CloudPrintJobUploadRequest(BaseModel): + ## Creates a new print job upload request. # \param job_name: The name of the print job. # \param file_size: The size of the file in bytes. diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py similarity index 88% rename from plugins/UM3NetworkPrinting/src/Models/CloudPrintResponse.py rename to plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py index 919d1b3c3a..b9f5b24d86 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudPrintResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py @@ -3,12 +3,12 @@ from datetime import datetime from typing import Optional, Union -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel # Model that represents the responses received from the cloud after requesting a job to be printed. -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudPrintResponse(BaseCloudModel): +class CloudPrintResponse(BaseModel): + ## Creates a new print response object. # \param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. # \param status: The status of the print request (queued or failed). diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py similarity index 50% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterBuildPlate.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py index 4386bbb435..fdc425fceb 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterBuildPlate.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py @@ -1,13 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel -## Class representing a cluster printer -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterBuildPlate(BaseCloudModel): +## Class representing a cluster printer +class ClusterBuildPlate(BaseModel): + ## Create a new build plate - # \param type: The type of buildplate glass or aluminium + # \param type: The type of build plate glass or aluminium def __init__(self, type: str = "glass", **kwargs) -> None: self.type = type super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py similarity index 82% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintCoreConfiguration.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index aba1cdb755..c27b1691d3 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -4,23 +4,23 @@ from typing import Union, Dict, Optional, Any from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel -from .CloudClusterPrinterConfigurationMaterial import CloudClusterPrinterConfigurationMaterial -from .BaseCloudModel import BaseCloudModel +from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMaterial +from ..BaseModel import BaseModel ## Class representing a cloud cluster printer configuration -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrintCoreConfiguration(BaseCloudModel): +class ClusterPrintCoreConfiguration(BaseModel): + ## Creates a new cloud cluster printer configuration object # \param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right. # \param material: The material of a configuration object in a cluster printer. May be in a dict or an object. # \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. def __init__(self, extruder_index: int, - material: Union[None, Dict[str, Any], CloudClusterPrinterConfigurationMaterial], + material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial], print_core_id: Optional[str] = None, **kwargs) -> None: self.extruder_index = extruder_index - self.material = self.parseModel(CloudClusterPrinterConfigurationMaterial, material) if material else None + self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None self.print_core_id = print_core_id super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py similarity index 87% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConfigurationChange.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py index 9ff4154666..eebc73a70e 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConfigurationChange.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py @@ -2,12 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel ## Model for the types of changes that are needed before a print job can start -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrintJobConfigurationChange(BaseCloudModel): +class ClusterPrintJobConfigurationChange(BaseModel): + ## Creates a new print job constraint. # \param type_of_change: The type of configuration change, one of: "material", "print_core_change" # \param index: The hotend slot or extruder index to change diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py similarity index 79% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConstraint.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py index 8236ec06b9..97e1b63abd 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobConstraint.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py @@ -2,12 +2,12 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel ## Class representing a cloud cluster print job constraint -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrintJobConstraints(BaseCloudModel): +class ClusterPrintJobConstraints(BaseModel): + ## Creates a new print job constraint. # \param require_printer_name: Unique name of the printer that this job should be printed on. # Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec' diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py similarity index 73% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobImpediment.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py index 12b67996c1..63038fe3f6 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobImpediment.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py @@ -1,13 +1,14 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from .BaseCloudModel import BaseCloudModel +from ..BaseModel import BaseModel ## Class representing the reasons that prevent this job from being printed on the associated printer -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrintJobImpediment(BaseCloudModel): +class ClusterPrintJobImpediment(BaseModel): + ## Creates a new print job constraint. - # \param translation_key: A string indicating a reason the print cannot be printed, such as 'does_not_fit_in_build_volume' + # \param translation_key: A string indicating a reason the print cannot be printed, + # such as 'does_not_fit_in_build_volume' # \param severity: A number indicating the severity of the problem, with higher being more severe def __init__(self, translation_key: str, severity: int, **kwargs) -> None: self.translation_key = translation_key diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py similarity index 77% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobStatus.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index 30f9b137f9..86bf02f9dd 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -3,20 +3,21 @@ from typing import List, Optional, Union, Dict, Any from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel -from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel -from plugins.UM3NetworkPrinting.src.Models.ConfigurationChangeModel import ConfigurationChangeModel -from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController -from .BaseCloudModel import BaseCloudModel -from .CloudClusterBuildPlate import CloudClusterBuildPlate -from .CloudClusterPrintJobConfigurationChange import CloudClusterPrintJobConfigurationChange -from .CloudClusterPrintJobImpediment import CloudClusterPrintJobImpediment -from .CloudClusterPrintCoreConfiguration import CloudClusterPrintCoreConfiguration -from .CloudClusterPrintJobConstraint import CloudClusterPrintJobConstraints + +from .ClusterBuildPlate import ClusterBuildPlate +from .ClusterPrintJobConfigurationChange import ClusterPrintJobConfigurationChange +from .ClusterPrintJobImpediment import ClusterPrintJobImpediment +from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration +from .ClusterPrintJobConstraint import ClusterPrintJobConstraints +from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel +from ..ConfigurationChangeModel import ConfigurationChangeModel +from ..BaseModel import BaseModel +from ...ClusterOutputController import ClusterOutputController ## Model for the status of a single print job in a cluster. -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrintJobStatus(BaseCloudModel): +class ClusterPrintJobStatus(BaseModel): + ## Creates a new cloud print job status model. # \param assigned_to: The name of the printer this job is assigned to while being queued. # \param configuration: The required print core configurations of this print job. @@ -45,21 +46,21 @@ class CloudClusterPrintJobStatus(BaseCloudModel): # printer def __init__(self, created_at: str, force: bool, machine_variant: str, name: str, started: bool, status: str, time_total: int, uuid: str, - configuration: List[Union[Dict[str, Any], CloudClusterPrintCoreConfiguration]], - constraints: List[Union[Dict[str, Any], CloudClusterPrintJobConstraints]], + configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], + constraints: List[Union[Dict[str, Any], ClusterPrintJobConstraints]], last_seen: Optional[float] = None, network_error_count: Optional[int] = None, owner: Optional[str] = None, printer_uuid: Optional[str] = None, time_elapsed: Optional[int] = None, assigned_to: Optional[str] = None, deleted_at: Optional[str] = None, printed_on_uuid: Optional[str] = None, configuration_changes_required: List[ - Union[Dict[str, Any], CloudClusterPrintJobConfigurationChange]] = None, - build_plate: Union[Dict[str, Any], CloudClusterBuildPlate] = None, + Union[Dict[str, Any], ClusterPrintJobConfigurationChange]] = None, + build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, compatible_machine_families: List[str] = None, - impediments_to_printing: List[Union[Dict[str, Any], CloudClusterPrintJobImpediment]] = None, + impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None, **kwargs) -> None: self.assigned_to = assigned_to - self.configuration = self.parseModels(CloudClusterPrintCoreConfiguration, configuration) - self.constraints = self.parseModels(CloudClusterPrintJobConstraints, constraints) + self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) + self.constraints = self.parseModels(ClusterPrintJobConstraints, constraints) self.created_at = created_at self.force = force self.last_seen = last_seen @@ -76,19 +77,19 @@ class CloudClusterPrintJobStatus(BaseCloudModel): self.deleted_at = deleted_at self.printed_on_uuid = printed_on_uuid - self.configuration_changes_required = self.parseModels(CloudClusterPrintJobConfigurationChange, + self.configuration_changes_required = self.parseModels(ClusterPrintJobConfigurationChange, configuration_changes_required) \ if configuration_changes_required else [] - self.build_plate = self.parseModel(CloudClusterBuildPlate, build_plate) if build_plate else None + self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None self.compatible_machine_families = compatible_machine_families if compatible_machine_families else [] - self.impediments_to_printing = self.parseModels(CloudClusterPrintJobImpediment, impediments_to_printing) \ + self.impediments_to_printing = self.parseModels(ClusterPrintJobImpediment, impediments_to_printing) \ if impediments_to_printing else [] super().__init__(**kwargs) ## Creates an UM3 print job output model based on this cloud cluster print job. # \param printer: The output model of the printer - def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel: + def createOutputModel(self, controller: ClusterOutputController) -> UM3PrintJobOutputModel: model = UM3PrintJobOutputModel(controller, self.uuid, self.name) self.updateOutputModel(model) return model diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py similarity index 92% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterConfigurationMaterial.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index db09133a14..1d0ef2b708 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -3,12 +3,13 @@ from typing import Optional from UM.Logger import Logger from cura.CuraApplication import CuraApplication from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel -from .BaseCloudModel import BaseCloudModel + +from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrinterConfigurationMaterial(BaseCloudModel): +## Class representing a cloud cluster printer configuration +class ClusterPrinterConfigurationMaterial(BaseModel): + ## Creates a new material configuration model. # \param brand: The brand of material in this print core, e.g. 'Ultimaker'. # \param color: The color of material in this print core, e.g. 'Blue'. diff --git a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py similarity index 84% rename from plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterStatus.py rename to plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 0b76ba1bce..43aa714521 100644 --- a/plugins/UM3NetworkPrinting/src/Models/CloudClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -4,14 +4,14 @@ from typing import List, Union, Dict, Optional, Any from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel -from .CloudClusterBuildPlate import CloudClusterBuildPlate -from .CloudClusterPrintCoreConfiguration import CloudClusterPrintCoreConfiguration -from .BaseCloudModel import BaseCloudModel +from .ClusterBuildPlate import ClusterBuildPlate +from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration +from ..BaseModel import BaseModel ## Class representing a cluster printer -# Spec: https://api-staging.ultimaker.com/connect/v1/spec -class CloudClusterPrinterStatus(BaseCloudModel): +class ClusterPrinterStatus(BaseModel): + ## Creates a new cluster printer status # \param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. # \param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. @@ -30,12 +30,12 @@ class CloudClusterPrinterStatus(BaseCloudModel): # \param build_plate: The build plate that is on the printer def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, status: str, unique_name: str, uuid: str, - configuration: List[Union[Dict[str, Any], CloudClusterPrintCoreConfiguration]], + configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, - build_plate: Union[Dict[str, Any], CloudClusterBuildPlate] = None, **kwargs) -> None: + build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, **kwargs) -> None: - self.configuration = self.parseModels(CloudClusterPrintCoreConfiguration, configuration) + self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.enabled = enabled self.firmware_version = firmware_version self.friendly_name = friendly_name @@ -48,7 +48,7 @@ class CloudClusterPrinterStatus(BaseCloudModel): self.maintenance_required = maintenance_required self.firmware_update_status = firmware_update_status self.latest_available_firmware = latest_available_firmware - self.build_plate = self.parseModel(CloudClusterBuildPlate, build_plate) if build_plate else None + self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None super().__init__(**kwargs) ## Creates a new output model. diff --git a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py index f3c3772a09..db9672cc29 100644 --- a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py @@ -3,13 +3,13 @@ from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel class LocalMaterial(BaseModel): + def __init__(self, GUID: str, id: str, version: int, **kwargs) -> None: self.GUID = GUID # type: str self.id = id # type: str self.version = version # type: int super().__init__(**kwargs) - # def validate(self) -> None: super().validate() if not self.GUID: diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 183829bb50..182e837091 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -1,10 +1,21 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Callable, List, Optional +import json +from json import JSONDecodeError +from typing import Callable, List, Optional, Dict, Union, Any, Type, cast, TypeVar, Tuple from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from UM.Logger import Logger +from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel + + +## The generic type variable used to document the methods below. +from plugins.UM3NetworkPrinting.src.Models.Http.ClusterPrinterStatus import ClusterPrinterStatus + +ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel) + ## The ClusterApiClient is responsible for all network calls to local network clusters. class ClusterApiClient: @@ -30,32 +41,95 @@ class ClusterApiClient: ## Get printer system information. # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: - url = f"{self.PRINTER_API_PREFIX}/system" + url = f"{self.PRINTER_API_PREFIX}/system/" + self._manager.get(self._createEmptyRequest(url)) + + ## Get the printers in the cluster. + # \param on_finished: The callback in case the response is successful. + def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: + url = f"{self.CLUSTER_API_PREFIX}/printers/" reply = self._manager.get(self._createEmptyRequest(url)) - self._addCallback(reply, on_finished) + self._addCallback(reply, on_finished, ClusterPrinterStatus) + + ## Get the print jobs in the cluster. + # \param on_finished: The callback in case the response is successful. + def getPrintJobs(self, on_finished: Callable) -> None: + url = f"{self.CLUSTER_API_PREFIX}/print_jobs/" + # reply = self._manager.get(self._createEmptyRequest(url)) + # self._addCallback(reply, on_finished) + + def requestPrint(self) -> None: + pass + + ## Send a print job action to the cluster. + # \param print_job_uuid: The UUID of the print job to perform the action on. + # \param action: The action to perform. + # \param data: The optional data to send along, used for 'move' and 'duplicate'. + def doPrintJobAction(self, print_job_uuid: str, action: str, data: Optional[Dict[str, Union[str, int]]] = None + ) -> None: + url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action/{action}/" + body = json.loads(data).encode() if data else b"" + self._manager.put(self._createEmptyRequest(url), body) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: - request = QNetworkRequest(QUrl(self._address + path)) + url = QUrl("http://" + self._address + path) + request = QNetworkRequest(url) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) return request + ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. + # \param reply: The reply from the server. + # \return A tuple with a status code and a dictionary. + @staticmethod + def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + try: + response = bytes(reply.readAll()).decode() + return status_code, json.loads(response) + except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: + Logger.logException("e", "Could not parse the cluster response: %s", err) + return status_code, {"errors": [err]} + + ## Parses the given models and calls the correct callback depending on the result. + # \param response: The response from the server, after being converted to a dict. + # \param on_finished: The callback in case the response is successful. + # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. + def _parseModels(self, response: Dict[str, Any], + on_finished: Union[Callable[[ClusterApiClientModel], Any], + Callable[[List[ClusterApiClientModel]], Any]], + model_class: Type[ClusterApiClientModel]) -> None: + if isinstance(response, list): + results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel] + on_finished_list = cast(Callable[[List[ClusterApiClientModel]], Any], on_finished) + on_finished_list(results) + else: + result = model_class(**response) # type: ClusterApiClientModel + on_finished_item = cast(Callable[[ClusterApiClientModel], Any], on_finished) + on_finished_item(result) + ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. - def _addCallback(self, reply: QNetworkReply, on_finished: Callable) -> None: + def _addCallback(self, + reply: QNetworkReply, + on_finished: Union[Callable[[ClusterApiClientModel], Any], + Callable[[List[ClusterApiClientModel]], Any]], + model: Type[ClusterApiClientModel], + ) -> None: def parse() -> None: # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: return - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - response = bytes(reply.readAll()).decode() + status_code, response = self._parseReply(reply) self._anti_gc_callbacks.remove(parse) - on_finished(int(status_code), response) + if on_finished: + self._parseModels(response, on_finished, model) return self._anti_gc_callbacks.append(parse) - reply.finished.connect(parse) + if on_finished: + reply.finished.connect(parse) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py index 898a2f6ae2..4cbe8b9194 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py @@ -1,218 +1,73 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, cast, Tuple, Union, Optional, Dict, List -from time import time +from typing import Optional, Dict, List, Any -import io # To create the correct buffers for sending data to the printer. -import json -import os +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty from UM.FileHandler.FileHandler import FileHandler -from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously. -from UM.i18n import i18nCatalog from UM.Logger import Logger -from UM.Message import Message -from UM.PluginRegistry import PluginRegistry -from UM.Scene.SceneNode import SceneNode # For typing. -from UM.Settings.ContainerRegistry import ContainerRegistry - -from cura.CuraApplication import CuraApplication -from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel -from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel -from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel -from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel +from UM.i18n import i18nCatalog +from UM.Scene.SceneNode import SceneNode +from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from plugins.UM3NetworkPrinting.src.Factories.PrinterModelFactory import PrinterModelFactory -from plugins.UM3NetworkPrinting.src.UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice -from plugins.UM3NetworkPrinting.src.Network.ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController -from plugins.UM3NetworkPrinting.src.MeshFormatHandler import MeshFormatHandler -from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob -from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel +from .ClusterApiClient import ClusterApiClient +from ..SendMaterialJob import SendMaterialJob +from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply -from PyQt5.QtGui import QDesktopServices, QImage -from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject -i18n_catalog = i18nCatalog("cura") +I18N_CATALOG = i18nCatalog("cura") class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): activeCameraUrlChanged = pyqtSignal() - def __init__(self, device_id, address, properties, parent = None) -> None: - super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent) - self._api_prefix = "/cluster-api/v1/" + def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], parent=None) -> None: - self._application = CuraApplication.getInstance() + super().__init__( + device_id=device_id, + address=address, + properties=properties, + connection_type=ConnectionType.NetworkConnection, + parent=parent + ) - self._number_of_extruders = 2 + # API client for making requests to the print cluster. + self._cluster_api = ClusterApiClient(address, on_error=self._onNetworkError) + # We don't have authentication over local networking, so we're always authenticated. + self.setAuthenticationState(AuthState.Authenticated) - self._dummy_lambdas = ( - "", {}, io.BytesIO() - ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]] - - if PluginRegistry.getInstance() is not None: - plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") - if plugin_path is None: - Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting") - raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting") - self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml") - - # Trigger the printersChanged signal when the private signal is triggered - self.printersChanged.connect(self._clusterPrintersChanged) - - self._accepts_commands = True # type: bool - - self._error_message = None # type: Optional[Message] - self._write_job_progress_message = None # type: Optional[Message] - self._progress_message = None # type: Optional[Message] - - self._printer_selection_dialog = None # type: QObject - - self.setPriority(3) # Make sure the output device gets selected above local file output - self.setName(self._id) - self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) - self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) - - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network")) - - self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] - - self._finished_jobs = [] # type: List[UM3PrintJobOutputModel] - - self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int - - self._latest_reply_handler = None # type: Optional[QNetworkReply] - self._sending_job = None + self._setInterfaceElements() self._active_camera_url = QUrl() # type: QUrl - def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: - self.writeStarted.emit(self) + # self._number_of_extruders = 2 + # self._dummy_lambdas = ( + # "", {}, io.BytesIO() + # ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]] + # self._error_message = None # type: Optional[Message] + # self._write_job_progress_message = None # type: Optional[Message] + # self._progress_message = None # type: Optional[Message] + # self._printer_selection_dialog = None # type: QObject + # self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] + # self._finished_jobs = [] # type: List[UM3PrintJobOutputModel] + # self._sending_job = None + ## Set all the interface elements and texts for this output device. + def _setInterfaceElements(self) -> None: + self.setPriority(3) # Make sure the output device gets selected above local file output + self.setName(self._id) + self.setShortDescription(I18N_CATALOG.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) + self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network")) + self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network")) + + ## Called when the connection to the cluster changes. + def connect(self) -> None: + super().connect() self.sendMaterialProfiles() - mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) - - # This function pauses with the yield, waiting on instructions on which printer it needs to print with. - if not mesh_format.is_valid: - Logger.log("e", "Missing file or mesh writer!") - return - self._sending_job = self._sendPrintJob(mesh_format, nodes) - if self._sending_job is not None: - self._sending_job.send(None) # Start the generator. - - if len(self._printers) > 1: # We need to ask the user. - self._spawnPrinterSelectionDialog() - else: # Just immediately continue. - self._sending_job.send("") # No specifically selected printer. - self._sending_job.send(None) - - def _spawnPrinterSelectionDialog(self): - if self._printer_selection_dialog is None: - if PluginRegistry.getInstance() is not None: - path = os.path.join( - PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - "resources", "qml", "PrintWindow.qml" - ) - self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self}) - if self._printer_selection_dialog is not None: - self._printer_selection_dialog.show() - - ## Allows the user to choose a printer to print with from the printer - # selection dialogue. - # \param target_printer The name of the printer to target. - @pyqtSlot(str) - def selectPrinter(self, target_printer: str = "") -> None: - if self._sending_job is not None: - self._sending_job.send(target_printer) - - @pyqtSlot() - def cancelPrintSelection(self) -> None: - self._sending_gcode = False - - ## Greenlet to send a job to the printer over the network. - # - # This greenlet gets called asynchronously in requestWrite. It is a - # greenlet in order to optionally wait for selectPrinter() to select a - # printer. - # The greenlet yields exactly three times: First time None, - # \param mesh_format Object responsible for choosing the right kind of format to write with. - def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]): - Logger.log("i", "Sending print job to printer.") - if self._sending_gcode: - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Sending new jobs (temporarily) blocked, still sending the previous print job.")) - self._error_message.show() - yield #Wait on the user to select a target printer. - yield #Wait for the write job to be finished. - yield False #Return whether this was a success or not. - yield #Prevent StopIteration. - - self._sending_gcode = True - - # Potentially wait on the user to select a target printer. - target_printer = yield # type: Optional[str] - - # Using buffering greatly reduces the write time for many lines of gcode - - stream = mesh_format.createStream() - - job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode) - - self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), - lifetime = 0, dismissable = False, progress = -1, - title = i18n_catalog.i18nc("@info:title", "Sending Data"), - use_inactivity_timer = False) - self._write_job_progress_message.show() - - if mesh_format.preferred_format is not None: - self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream) - job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) - job.start() - yield True # Return that we had success! - yield # To prevent having to catch the StopIteration exception. - - def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: - if self._write_job_progress_message: - self._write_job_progress_message.hide() - - self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, - dismissable = False, progress = -1, - title = i18n_catalog.i18nc("@info:title", "Sending Data")) - self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = "", - description = "") - self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) - self._progress_message.show() - parts = [] - - target_printer, preferred_format, stream = self._dummy_lambdas - - # If a specific printer was selected, it should be printed with that machine. - if target_printer: - target_printer = self._printer_uuid_to_unique_name_mapping[target_printer] - parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) - - # Add user name to the print_job - parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) - - file_name = self._application.getPrintInformation().jobName + "." + preferred_format["extension"] - - output = stream.getvalue() # Either str or bytes depending on the output mode. - if isinstance(stream, io.StringIO): - output = cast(str, output).encode("utf-8") - output = cast(bytes, output) - - parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) - - self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, - on_finished = self._onPostPrintJobFinished, - on_progress = self._onUploadPrintJobProgress) - @pyqtProperty(QUrl, notify=activeCameraUrlChanged) def activeCameraUrl(self) -> QUrl: return self._active_camera_url @@ -223,63 +78,6 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): self._active_camera_url = camera_url self.activeCameraUrlChanged.emit() - ## The IP address of the printer. - @pyqtProperty(str, constant = True) - def address(self) -> str: - return self._address - - def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: - if self._progress_message: - self._progress_message.hide() - self._compressing_gcode = False - self._sending_gcode = False - - def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: - if bytes_total > 0: - new_progress = bytes_sent / bytes_total * 100 - # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get - # timeout responses if this happens. - self._last_response_time = time() - if self._progress_message is not None and new_progress != self._progress_message.getProgress(): - self._progress_message.show() # Ensure that the message is visible. - self._progress_message.setProgress(bytes_sent / bytes_total * 100) - - # If successfully sent: - if bytes_sent == bytes_total: - # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to - # the monitor tab. - self._success_message = Message( - i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), - lifetime=5, dismissable=True, - title=i18n_catalog.i18nc("@info:title", "Data Sent")) - self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon = "", - description="") - self._success_message.actionTriggered.connect(self._successMessageActionTriggered) - self._success_message.show() - else: - if self._progress_message is not None: - self._progress_message.setProgress(0) - self._progress_message.hide() - - def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: - if action_id == "Abort": - Logger.log("d", "User aborted sending print to remote.") - if self._progress_message is not None: - self._progress_message.hide() - self._compressing_gcode = False - self._sending_gcode = False - self._application.getController().setActiveStage("PrepareStage") - - # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request - # the "reply" should be disconnected - if self._latest_reply_handler: - self._latest_reply_handler.disconnect() - self._latest_reply_handler = None - - def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: - if action_id == "View": - self._application.getController().setActiveStage("MonitorStage") - @pyqtSlot(name="openPrintJobControlPanel") def openPrintJobControlPanel(self) -> None: QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs")) @@ -290,338 +88,282 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="sendJobToTop") def sendJobToTop(self, print_job_uuid: str) -> None: - # This function is part of the output device (and not of the printjob output model) as this type of operation - # is a modification of the cluster queue and not of the actual job. - data = "{\"to_position\": 0}" - self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None) + self._cluster_api.doPrintJobAction(print_job_uuid, "move", {"to_position": 0, "list": "queued"}) @pyqtSlot(str, name="deleteJobFromQueue") def deleteJobFromQueue(self, print_job_uuid: str) -> None: - # This function is part of the output device (and not of the printjob output model) as this type of operation - # is a modification of the cluster queue and not of the actual job. - self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None) + self._cluster_api.doPrintJobAction(print_job_uuid, "delete") @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: - data = "{\"force\": true}" - self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None) + self._cluster_api.doPrintJobAction(print_job_uuid, "force") - # Set the remote print job state. - def setJobState(self, print_job_uuid: str, state: str) -> None: - # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. - action = "print" if state == "resume" else state - data = "{\"action\": \"%s\"}" % action - self.put("print_jobs/%s/action" % print_job_uuid, data, on_finished=None) + ## Set the remote print job state. + # \param print_job_uuid: The UUID of the print job to set the state for. + # \param action: The action to undertake ('pause', 'resume', 'abort'). + def setJobState(self, print_job_uuid: str, action: str) -> None: + self._cluster_api.doPrintJobAction(print_job_uuid, action) - def _printJobStateChanged(self) -> None: - username = self._getUserName() - - if username is None: - return # We only want to show notifications if username is set. - - finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"] - - newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username] - for job in newly_finished_jobs: - if job.assignedPrinter: - job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(printer_name=job.assignedPrinter.name, job_name = job.name) - else: - job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.").format(job_name = job.name) - job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished")) - job_completed_message.show() - - # Ensure UI gets updated - self.printJobsChanged.emit() - - # Keep a list of all completed jobs so we know if something changed next time. - self._finished_jobs = finished_jobs - - ## Called when the connection to the cluster changes. - def connect(self) -> None: - super().connect() - self.sendMaterialProfiles() - - def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: - reply_url = reply.url().toString() - - uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")] - - print_job = findByKey(self._print_jobs, uuid) - if print_job: - image = QImage() - image.loadFromData(reply.readAll()) - print_job.updatePreviewImage(image) + ## Handle network errors. + @staticmethod + def _onNetworkError(errors: Dict[str, Any]): + Logger.log("w", str(errors)) def _update(self) -> None: super()._update() - self.get("printers/", on_finished = self._onGetPrintersDataFinished) - self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished) - - for print_job in self._print_jobs: - if print_job.getPreviewImage() is None: - self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) - - def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: - if not checkValidGetReply(reply): - return - - result = loadJsonFromReply(reply) - if result is None: - return - - print_jobs_seen = [] - job_list_changed = False - for idx, print_job_data in enumerate(result): - print_job = findByKey(self._print_jobs, print_job_data["uuid"]) - if print_job is None: - print_job = self._createPrintJobModel(print_job_data) - job_list_changed = True - elif not job_list_changed: - # Check if the order of the jobs has changed since the last check - if self._print_jobs.index(print_job) != idx: - job_list_changed = True - - self._updatePrintJob(print_job, print_job_data) - - if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer. - if print_job.state in ["failed", "finished", "aborted", "none"]: - # Print job was already completed, so don't attach it to a printer. - printer = None - else: - printer = self._getPrinterByKey(print_job_data["printer_uuid"]) - else: # The job can "reserve" a printer if some changes are required. - printer = self._getPrinterByKey(print_job_data["assigned_to"]) - - if printer: - printer.updateActivePrintJob(print_job) - - print_jobs_seen.append(print_job) - - # Check what jobs need to be removed. - removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen] - - for removed_job in removed_jobs: - job_list_changed = job_list_changed or self._removeJob(removed_job) - - if job_list_changed: - # Override the old list with the new list (either because jobs were removed / added or order changed) - self._print_jobs = print_jobs_seen - self.printJobsChanged.emit() # Do a single emit for all print job changes. - - def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: - if not checkValidGetReply(reply): - return - - result = loadJsonFromReply(reply) - if result is None: - return - - printer_list_changed = False - printers_seen = [] - - for printer_data in result: - printer = findByKey(self._printers, printer_data["uuid"]) - - if printer is None: - output_controller = ClusterUM3PrinterOutputController(self) - printer = PrinterModelFactory.createPrinter(output_controller=output_controller, - ip_address=printer_data.get("ip_address", ""), - extruder_count=self._number_of_extruders) - self._printers.append(printer) - printer_list_changed = True - - printers_seen.append(printer) - - self._updatePrinter(printer, printer_data) - - removed_printers = [printer for printer in self._printers if printer not in printers_seen] - for printer in removed_printers: - self._removePrinter(printer) - - if removed_printers or printer_list_changed: - self.printersChanged.emit() - - def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel: - print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), - key=data["uuid"], name= data["name"]) - - configuration = PrinterConfigurationModel() - extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)] - for index in range(0, self._number_of_extruders): - try: - extruder_data = data["configuration"][index] - except IndexError: - continue - extruder = extruders[int(data["configuration"][index]["extruder_index"])] - extruder.setHotendID(extruder_data.get("print_core_id", "")) - extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {}))) - - configuration.setExtruderConfigurations(extruders) - configuration.setPrinterType(data.get("machine_variant", "")) - print_job.updateConfiguration(configuration) - print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", [])) - print_job.stateChanged.connect(self._printJobStateChanged) - return print_job - - def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None: - print_job.updateTimeTotal(data["time_total"]) - print_job.updateTimeElapsed(data["time_elapsed"]) - impediments_to_printing = data.get("impediments_to_printing", []) - print_job.updateOwner(data["owner"]) - - status_set_by_impediment = False - for impediment in impediments_to_printing: - if impediment["severity"] == "UNFIXABLE": - status_set_by_impediment = True - print_job.updateState("error") - break - - if not status_set_by_impediment: - print_job.updateState(data["status"]) - - configuration_changes = PrinterModelFactory.createConfigurationChanges(data.get("configuration_changes_required")) - print_job.updateConfigurationChanges(configuration_changes) - - def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel": - material_manager = self._application.getMaterialManager() - material_group_list = None - - # Avoid crashing if there is no "guid" field in the metadata - material_guid = material_data.get("guid") - if material_guid: - material_group_list = material_manager.getMaterialGroupListByGUID(material_guid) - - # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the - # material is unknown to Cura, so we should return an "empty" or "unknown" material model. - if material_group_list is None: - material_name = i18n_catalog.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \ - else i18n_catalog.i18nc("@label:material", "Unknown") - - return MaterialOutputModel(guid = material_data.get("guid", ""), - type = material_data.get("material", ""), - color = material_data.get("color", ""), - brand = material_data.get("brand", ""), - name = material_data.get("name", material_name) - ) - - # Sort the material groups by "is_read_only = True" first, and then the name alphabetically. - read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list)) - non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list)) - material_group = None - if read_only_material_group_list: - read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name) - material_group = read_only_material_group_list[0] - elif non_read_only_material_group_list: - non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name) - material_group = non_read_only_material_group_list[0] - - if material_group: - container = material_group.root_material_node.getContainer() - color = container.getMetaDataEntry("color_code") - brand = container.getMetaDataEntry("brand") - material_type = container.getMetaDataEntry("material") - name = container.getName() - else: - Logger.log("w", - "Unable to find material with guid {guid}. Using data as provided by cluster".format( - guid=material_data["guid"])) - color = material_data["color"] - brand = material_data["brand"] - material_type = material_data["material"] - name = i18n_catalog.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \ - else i18n_catalog.i18nc("@label:material", "Unknown") - return MaterialOutputModel(guid = material_data["guid"], type = material_type, - brand = brand, color = color, name = name) - - def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None: - # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. - # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. - self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"] - - definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"]) - if not definitions: - Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"]) - return - - machine_definition = definitions[0] - - printer.updateName(data["friendly_name"]) - printer.updateKey(data["uuid"]) - printer.updateType(data["machine_variant"]) - - if data["status"] != "unreachable": - self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(data["ip_address"], - name = data["friendly_name"], - machine_type = data["machine_variant"]) - - # Do not store the build plate information that comes from connect if the current printer has not build plate information - if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False): - printer.updateBuildplate(data["build_plate"]["type"]) - if not data["enabled"]: - printer.updateState("disabled") - else: - printer.updateState(data["status"]) - - for index in range(0, self._number_of_extruders): - extruder = printer.extruders[index] - try: - extruder_data = data["configuration"][index] - except IndexError: - break - - extruder.updateHotendID(extruder_data.get("print_core_id", "")) - - material_data = extruder_data["material"] - if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: - material = self._createMaterialOutputModel(material_data) - extruder.updateActiveMaterial(material) - - def _removeJob(self, job: UM3PrintJobOutputModel) -> bool: - if job not in self._print_jobs: - return False - - if job.assignedPrinter: - job.assignedPrinter.updateActivePrintJob(None) - job.stateChanged.disconnect(self._printJobStateChanged) - self._print_jobs.remove(job) - - return True - - def _removePrinter(self, printer: PrinterOutputModel) -> None: - self._printers.remove(printer) - if self._active_printer == printer: - self._active_printer = None - self.activePrinterChanged.emit() + self._cluster_api.getPrinters(self._updatePrinters) + self._cluster_api.getPrintJobs(self._updatePrintJobs) + # for print_job in self._print_jobs: + # if print_job.getPreviewImage() is None: + # self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) ## Sync the material profiles in Cura with the printer. - # - # This gets called when connecting to a printer as well as when sending a - # print. + # This gets called when connecting to a printer as well as when sending a print. def sendMaterialProfiles(self) -> None: - job = SendMaterialJob(device = self) + job = SendMaterialJob(device=self) job.run() -def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]: - try: - result = json.loads(bytes(reply.readAll()).decode("utf-8")) - except json.decoder.JSONDecodeError: - Logger.logException("w", "Unable to decode JSON from reply.") - return None - return result + + # TODO FROM HERE -def checkValidGetReply(reply: QNetworkReply) -> bool: - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if status_code != 200: - Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code)) - return False - return True + def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: + pass + # self.writeStarted.emit(self) + # + # self.sendMaterialProfiles() + # + # mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) + # + # # This function pauses with the yield, waiting on instructions on which printer it needs to print with. + # if not mesh_format.is_valid: + # Logger.log("e", "Missing file or mesh writer!") + # return + # self._sending_job = self._sendPrintJob(mesh_format, nodes) + # if self._sending_job is not None: + # self._sending_job.send(None) # Start the generator. + # + # if len(self._printers) > 1: # We need to ask the user. + # self._spawnPrinterSelectionDialog() + # else: # Just immediately continue. + # self._sending_job.send("") # No specifically selected printer. + # self._sending_job.send(None) + # + # def _spawnPrinterSelectionDialog(self): + # if self._printer_selection_dialog is None: + # if PluginRegistry.getInstance() is not None: + # path = os.path.join( + # PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), + # "resources", "qml", "PrintWindow.qml" + # ) + # self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self}) + # if self._printer_selection_dialog is not None: + # self._printer_selection_dialog.show() + # ## Allows the user to choose a printer to print with from the printer + # # selection dialogue. + # # \param target_printer The name of the printer to target. + # @pyqtSlot(str) + # def selectPrinter(self, target_printer: str = "") -> None: + # if self._sending_job is not None: + # self._sending_job.send(target_printer) -def findByKey(lst: List[Union[UM3PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[UM3PrintJobOutputModel]: - for item in lst: - if item.key == key: - return item - return None + # @pyqtSlot() + # def cancelPrintSelection(self) -> None: + # self._sending_gcode = False + + # ## Greenlet to send a job to the printer over the network. + # # + # # This greenlet gets called asynchronously in requestWrite. It is a + # # greenlet in order to optionally wait for selectPrinter() to select a + # # printer. + # # The greenlet yields exactly three times: First time None, + # # \param mesh_format Object responsible for choosing the right kind of format to write with. + # def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]): + # Logger.log("i", "Sending print job to printer.") + # if self._sending_gcode: + # self._error_message = Message( + # I18N_CATALOG.i18nc("@info:status", + # "Sending new jobs (temporarily) blocked, still sending the previous print job.")) + # self._error_message.show() + # yield #Wait on the user to select a target printer. + # yield #Wait for the write job to be finished. + # yield False #Return whether this was a success or not. + # yield #Prevent StopIteration. + # + # self._sending_gcode = True + # + # # Potentially wait on the user to select a target printer. + # target_printer = yield # type: Optional[str] + # + # # Using buffering greatly reduces the write time for many lines of gcode + # + # stream = mesh_format.createStream() + # + # job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode) + # + # self._write_job_progress_message = Message(I18N_CATALOG.i18nc("@info:status", "Sending data to printer"), + # lifetime = 0, dismissable = False, progress = -1, + # title = I18N_CATALOG.i18nc("@info:title", "Sending Data"), + # use_inactivity_timer = False) + # self._write_job_progress_message.show() + # + # if mesh_format.preferred_format is not None: + # self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream) + # job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) + # job.start() + # yield True # Return that we had success! + # yield # To prevent having to catch the StopIteration exception. + + # def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: + # if self._write_job_progress_message: + # self._write_job_progress_message.hide() + # + # self._progress_message = Message(I18N_CATALOG.i18nc("@info:status", "Sending data to printer"), lifetime = 0, + # dismissable = False, progress = -1, + # title = I18N_CATALOG.i18nc("@info:title", "Sending Data")) + # self._progress_message.addAction("Abort", I18N_CATALOG.i18nc("@action:button", "Cancel"), icon = "", + # description = "") + # self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) + # self._progress_message.show() + # parts = [] + # + # target_printer, preferred_format, stream = self._dummy_lambdas + # + # # If a specific printer was selected, it should be printed with that machine. + # if target_printer: + # target_printer = self._printer_uuid_to_unique_name_mapping[target_printer] + # parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) + # + # # Add user name to the print_job + # parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) + # + # file_name = self._application.getPrintInformation().jobName + "." + preferred_format["extension"] + # + # output = stream.getvalue() # Either str or bytes depending on the output mode. + # if isinstance(stream, io.StringIO): + # output = cast(str, output).encode("utf-8") + # output = cast(bytes, output) + # + # parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) + # + # self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, + # on_finished = self._onPostPrintJobFinished, + # on_progress = self._onUploadPrintJobProgress) + + # def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: + # if self._progress_message: + # self._progress_message.hide() + # self._compressing_gcode = False + # self._sending_gcode = False + + # def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: + # if bytes_total > 0: + # new_progress = bytes_sent / bytes_total * 100 + # # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get + # # timeout responses if this happens. + # self._last_response_time = time() + # if self._progress_message is not None and new_progress != self._progress_message.getProgress(): + # self._progress_message.show() # Ensure that the message is visible. + # self._progress_message.setProgress(bytes_sent / bytes_total * 100) + # + # # If successfully sent: + # if bytes_sent == bytes_total: + # # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to + # # the monitor tab. + # self._success_message = Message( + # I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), + # lifetime=5, dismissable=True, + # title=I18N_CATALOG.i18nc("@info:title", "Data Sent")) + # self._success_message.addAction("View", I18N_CATALOG.i18nc("@action:button", "View in Monitor"), icon = "", + # description="") + # self._success_message.actionTriggered.connect(self._successMessageActionTriggered) + # self._success_message.show() + # else: + # if self._progress_message is not None: + # self._progress_message.setProgress(0) + # self._progress_message.hide() + + # def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: + # if action_id == "Abort": + # Logger.log("d", "User aborted sending print to remote.") + # if self._progress_message is not None: + # self._progress_message.hide() + # self._compressing_gcode = False + # self._sending_gcode = False + # self._application.getController().setActiveStage("PrepareStage") + # + # # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request + # # the "reply" should be disconnected + # if self._latest_reply_handler: + # self._latest_reply_handler.disconnect() + # self._latest_reply_handler = None + + # def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: + # if action_id == "View": + # self._application.getController().setActiveStage("MonitorStage") + + # def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: + # reply_url = reply.url().toString() + # + # uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")] + # + # print_job = findByKey(self._print_jobs, uuid) + # if print_job: + # image = QImage() + # image.loadFromData(reply.readAll()) + # print_job.updatePreviewImage(image) + + # def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel": + # material_manager = self._application.getMaterialManager() + # material_group_list = None + # + # # Avoid crashing if there is no "guid" field in the metadata + # material_guid = material_data.get("guid") + # if material_guid: + # material_group_list = material_manager.getMaterialGroupListByGUID(material_guid) + # + # # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the + # # material is unknown to Cura, so we should return an "empty" or "unknown" material model. + # if material_group_list is None: + # material_name = I18N_CATALOG.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \ + # else I18N_CATALOG.i18nc("@label:material", "Unknown") + # + # return MaterialOutputModel(guid = material_data.get("guid", ""), + # type = material_data.get("material", ""), + # color = material_data.get("color", ""), + # brand = material_data.get("brand", ""), + # name = material_data.get("name", material_name) + # ) + # + # # Sort the material groups by "is_read_only = True" first, and then the name alphabetically. + # read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list)) + # non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list)) + # material_group = None + # if read_only_material_group_list: + # read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name) + # material_group = read_only_material_group_list[0] + # elif non_read_only_material_group_list: + # non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name) + # material_group = non_read_only_material_group_list[0] + # + # if material_group: + # container = material_group.root_material_node.getContainer() + # color = container.getMetaDataEntry("color_code") + # brand = container.getMetaDataEntry("brand") + # material_type = container.getMetaDataEntry("material") + # name = container.getName() + # else: + # Logger.log("w", + # "Unable to find material with guid {guid}. Using data as provided by cluster".format( + # guid=material_data["guid"])) + # color = material_data["color"] + # brand = material_data["brand"] + # material_type = material_data["material"] + # name = I18N_CATALOG.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \ + # else I18N_CATALOG.i18nc("@label:material", "Unknown") + # return MaterialOutputModel(guid = material_data["guid"], type = material_type, + # brand = brand, color = color, name = name) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/src/Network/ClusterUM3PrinterOutputController.py deleted file mode 100644 index 103be8b01e..0000000000 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3PrinterOutputController.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from cura.PrinterOutput.PrinterOutputController import PrinterOutputController - -MYPY = False -if MYPY: - from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel - - -class ClusterUM3PrinterOutputController(PrinterOutputController): - def __init__(self, output_device): - super().__init__(output_device) - self.can_pre_heat_bed = False - self.can_pre_heat_hotends = False - self.can_control_manually = False - self.can_send_raw_gcode = False - - def setJobState(self, job: "PrintJobOutputModel", state: str) -> None: - self._output_device.setJobState(job.key, state) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py b/plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py deleted file mode 100644 index 1a72e7ff70..0000000000 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - - -## The network API client is responsible for handling requests and responses to printer over the local network (LAN). -class NetworkApiClient: - - API_PREFIX = "/cluster-api/v1/" - - def __init__(self) -> None: - pass - - def getPrinters(self): - pass - - def getPrintJobs(self): - pass - - def requestPrint(self): - pass - - def doPrintJobAction(self): - pass diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 613ad1a0d6..e4f61ba091 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -61,7 +61,6 @@ class NetworkOutputDeviceManager: return um_network_key = active_machine.getMetaDataEntry("um_network_key") - for key in self._discovered_devices: if key == um_network_key: if not self._discovered_devices[key].isConnected(): @@ -229,6 +228,7 @@ class NetworkOutputDeviceManager: global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"): # Ensure that the configured connection type is set. + CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) global_container_stack.addConfiguredConnectionType(device.connectionType.value) device.connect() device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged) @@ -371,8 +371,6 @@ class NetworkOutputDeviceManager: # Ensure that these containers do know that they are configured for network connection machine.addConfiguredConnectionType(printer_device.connectionType.value) - self.refreshConnections() - ## Create a machine instance based on the discovered network printer. def _createMachineFromDiscoveredPrinter(self, key: str) -> None: discovered_device = self._discovered_devices.get(key) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index f2a1df3a4d..0298518ba9 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -1,14 +1,22 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import os from typing import List, Optional, Dict from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot, QUrl +from UM.Logger import Logger from UM.Qt.Duration import Duration, DurationFormat +from cura.CuraApplication import CuraApplication from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState -from plugins.UM3NetworkPrinting.src.Utils import formatTimeCompleted, formatDateCompleted -from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel +from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from plugins.UM3NetworkPrinting.src.Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus + +from .Utils import formatTimeCompleted, formatDateCompleted +from .ClusterOutputController import ClusterOutputController +from .Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel +from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus ## Output device class that forms the basis of Ultimaker networked printer output devices. @@ -29,13 +37,17 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # States indicating if a print job is queued. QUEUED_PRINT_JOBS_STATES = {"queued", "error"} - def __init__(self, device_id, address, properties, connection_type, parent=None) -> None: + def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType, + parent=None) -> None: super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, parent=parent) # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) + # Keeps track of all printers in the cluster. + self._printers = [] # type: List[PrinterOutputModel] + # Keeps track of all print jobs in the cluster. self._print_jobs = [] # type: List[UM3PrintJobOutputModel] @@ -45,6 +57,14 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # By default we are not authenticated. This state will be changed later. self._authentication_state = AuthState.NotAuthenticated + # Load the Monitor UI elements. + self._loadMonitorTab() + + ## The IP address of the printer. + @pyqtProperty(str, constant=True) + def address(self) -> str: + return self._address + # Get all print jobs for this cluster. @pyqtProperty("QVariantList", notify=printJobsChanged) def printJobs(self) -> List[UM3PrintJobOutputModel]: @@ -150,3 +170,93 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): @pyqtSlot(int, result=str, name="formatDuration") def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) + + ## Load Monitor tab QML. + def _loadMonitorTab(self): + plugin_registry = CuraApplication.getInstance().getPluginRegistry() + if not plugin_registry: + Logger.log("e", "Could not get plugin registry") + return + plugin_path = plugin_registry.getPluginPath("UM3NetworkPrinting") + if not plugin_path: + Logger.log("e", "Could not get plugin path") + return + self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml") + + def _updatePrinters(self, remote_printers: List[ClusterPrinterStatus]) -> None: + + # Keep track of the new printers to show. + # We create a new list instead of changing the existing one to get the correct order. + new_printers = [] + + # Check which printers need to be created or updated. + for index, printer_data in enumerate(remote_printers): + printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid), None) + if not printer: + printer = printer_data.createOutputModel(ClusterOutputController(self)) + else: + printer_data.updateOutputModel(printer) + new_printers.append(printer) + + # Check which printers need to be removed (de-referenced). + remote_printers_keys = [printer_data.uuid for printer_data in remote_printers] + removed_printers = [printer for printer in self._printers if printer.key not in remote_printers_keys] + for removed_printer in removed_printers: + if self._active_printer and self._active_printer.key == removed_printer.key: + self.setActivePrinter(None) + + self._printers = new_printers + if self._printers and not self.activePrinter: + self.setActivePrinter(self._printers[0]) + + self.printersChanged.emit() + + ## Updates the local list of print jobs with the list received from the cloud. + # \param remote_jobs: The print jobs received from the cloud. + def _updatePrintJobs(self, remote_jobs: List[ClusterPrintJobStatus]) -> None: + + # Keep track of the new print jobs to show. + # We create a new list instead of changing the existing one to get the correct order. + new_print_jobs = [] + + # Check which print jobs need to be created or updated. + for index, print_job_data in enumerate(remote_jobs): + print_job = next( + iter(print_job for print_job in self._print_jobs if print_job.key == print_job_data.uuid), None) + if not print_job: + new_print_jobs.append(self._createPrintJobModel(print_job_data)) + else: + print_job_data.updateOutputModel(print_job) + if print_job_data.printer_uuid: + self._updateAssignedPrinter(print_job, print_job_data.printer_uuid) + new_print_jobs.append(print_job) + + # Check which print job need to be removed (de-referenced). + remote_job_keys = [print_job_data.uuid for print_job_data in remote_jobs] + removed_jobs = [print_job for print_job in self._print_jobs if print_job.key not in remote_job_keys] + for removed_job in removed_jobs: + if removed_job.assignedPrinter: + removed_job.assignedPrinter.updateActivePrintJob(None) + removed_job.stateChanged.disconnect(self._onPrintJobStateChanged) + + self._print_jobs = new_print_jobs + self.printJobsChanged.emit() + + ## Create a new print job model based on the remote status of the job. + # \param remote_job: The remote print job data. + def _createPrintJobModel(self, remote_job: ClusterPrintJobStatus) -> UM3PrintJobOutputModel: + model = remote_job.createOutputModel(ClusterOutputController(self)) + model.stateChanged.connect(self._onPrintJobStateChanged) + if remote_job.printer_uuid: + self._updateAssignedPrinter(model, remote_job.printer_uuid) + return model + + ## Updates the printer assignment for the given print job model. + def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: + printer = next((p for p in self._printers if printer_uuid == p.key), None) + if not printer: + Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key, + [p.key for p in self._printers]) + return + printer.updateActivePrintJob(model) + model.updateAssignedPrinter(printer) From 7d69b1727d246a98024079250ec66795376115cc Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 14:56:21 +0200 Subject: [PATCH 06/52] Simplify code --- .../resources/qml/MonitorPrintJobCard.qml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml index ea6da9c25d..5aeedfd4ca 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml @@ -1,6 +1,5 @@ // Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. - import QtQuick 2.2 import QtQuick.Controls 2.0 import UM 1.3 as UM @@ -76,6 +75,7 @@ Item anchors.verticalCenter: parent.verticalCenter height: 18 * screenScaleFactor // TODO: Theme! width: UM.Theme.getSize("monitor_column").width + Rectangle { color: UM.Theme.getColor("monitor_skeleton_loading") @@ -84,6 +84,7 @@ Item visible: !printJob radius: 2 * screenScaleFactor // TODO: Theme! } + Label { text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : "" @@ -179,13 +180,10 @@ Item id: printerConfiguration anchors.verticalCenter: parent.verticalCenter buildplate: catalog.i18nc("@label", "Glass") - configurations: - [ - base.printJob.configuration.extruderConfigurations[0], - base.printJob.configuration.extruderConfigurations[1] - ] + configurations: base.printJob.configuration.extruderConfigurations height: 72 * screenScaleFactor // TODO: Theme! } + Label { text: printJob && printJob.owner ? printJob.owner : "" color: UM.Theme.getColor("monitor_text_primary") From ddd282eef32874158e11607e959d87f78f5506ee Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 16:11:01 +0200 Subject: [PATCH 07/52] Remove absolute plugin imports, some fixes --- .../Models/DiscoveredPrintersModel.py | 2 +- .../src/Cloud/CloudApiClient.py | 11 +++----- .../src/Models/BaseModel.py | 7 ++--- .../src/Models/ClusterMaterial.py | 2 +- .../src/Models/ConfigurationChangeModel.py | 5 ++-- .../src/Models/LocalMaterial.py | 2 +- .../src/Models/UM3PrintJobOutputModel.py | 5 ++-- .../src/Network/ClusterApiClient.py | 27 +++++++++++++------ ...tDevice.py => LocalClusterOutputDevice.py} | 6 ++--- .../src/Network/NetworkOutputDeviceManager.py | 11 ++++---- .../UM3NetworkPrinting/src/SendMaterialJob.py | 12 ++++----- .../src/UM3OutputDevicePlugin.py | 12 +-------- .../UltimakerNetworkedPrinterOutputDevice.py | 9 ++++--- .../tests/Cloud/TestCloudApiClient.py | 12 +++++---- .../tests/Cloud/TestCloudOutputDevice.py | 2 +- .../Cloud/TestCloudOutputDeviceManager.py | 4 ++- 16 files changed, 67 insertions(+), 62 deletions(-) rename plugins/UM3NetworkPrinting/src/Network/{ClusterUM3OutputDevice.py => LocalClusterOutputDevice.py} (98%) diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index 803e1d21ba..33e0b7a4d9 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject): def readableMachineType(self) -> str: from cura.CuraApplication import CuraApplication machine_manager = CuraApplication.getInstance().getMachineManager() - # In ClusterUM3OutputDevice, when it updates a printer information, it updates the machine type using the field + # In LocalClusterOutputDevice, when it updates a printer information, it updates the machine type using the field # "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string # like "Ultimaker 3". The code below handles this case. if self._hasHumanReadableMachineTypeName(self._machine_type): diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index bd61b945cf..12079dc497 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -101,14 +101,9 @@ class CloudApiClient: # \param cluster_id: The ID of the cluster. # \param cluster_job_id: The ID of the print job within the cluster. # \param action: The name of the action to execute. - def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None: - body = b"" - if data: - try: - body = json.dumps({"data": data}).encode() - except JSONDecodeError as err: - Logger.log("w", "Could not encode body: %s", err) - return + def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None + ) -> None: + body = json.dumps({"data": data}).encode() if data else b"" url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) self._manager.post(self._createEmptyRequest(url), body) diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index 2c5c667f89..131b71ab43 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -3,6 +3,10 @@ from datetime import datetime, timezone from typing import TypeVar, Dict, List, Any, Type, Union +# Type variable used in the parse methods below, which should be a subclass of BaseModel. +T = TypeVar("T", bound="BaseModel") + + class BaseModel: def __init__(self, **kwargs) -> None: @@ -29,9 +33,6 @@ class BaseModel: def toDict(self) -> Dict[str, Any]: return self.__dict__ - # Type variable used in the parse methods below, which should be a subclass of BaseModel. - T = TypeVar("T", bound="BaseModel") - ## Parses a single model. # \param model_class: The model class. # \param values: The value of the model, which is usually a dictionary, but may also be already parsed. diff --git a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py index 37e4ed390f..8687b015ce 100644 --- a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py @@ -1,5 +1,5 @@ ## Class representing a material that was fetched from the cluster API. -from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel +from .BaseModel import BaseModel class ClusterMaterial(BaseModel): diff --git a/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py b/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py index 3521b55f63..7b81f6e431 100644 --- a/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py @@ -1,8 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - from PyQt5.QtCore import pyqtProperty, QObject + BLOCKING_CHANGE_TYPES = [ "material_insert", "buildplate_change" ] @@ -11,8 +11,7 @@ BLOCKING_CHANGE_TYPES = [ class ConfigurationChangeModel(QObject): def __init__(self, type_of_change: str, index: int, target_name: str, origin_name: str) -> None: super().__init__() - self._type_of_change = type_of_change - # enum = ["material", "print_core_change"] + self._type_of_change = type_of_change # enum = ["material", "print_core_change"] self._can_override = self._type_of_change not in BLOCKING_CHANGE_TYPES self._index = index self._target_name = target_name diff --git a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py index db9672cc29..9e606fd1c4 100644 --- a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py @@ -1,5 +1,5 @@ ## Class representing a local material that was fetched from the container registry. -from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel +from .BaseModel import BaseModel class LocalMaterial(BaseModel): diff --git a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py index 0112ab94eb..3462fc4a54 100644 --- a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py @@ -6,13 +6,14 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController -from plugins.UM3NetworkPrinting.src.Models.ConfigurationChangeModel import ConfigurationChangeModel + +from .ConfigurationChangeModel import ConfigurationChangeModel class UM3PrintJobOutputModel(PrintJobOutputModel): configurationChangesChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: + def __init__(self, output_controller: PrinterOutputController, key: str = "", name: str = "", parent=None) -> None: super().__init__(output_controller, key, name, parent) self._configuration_changes = [] # type: List[ConfigurationChangeModel] diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 182e837091..e6a816abcd 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -8,12 +8,13 @@ from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from UM.Logger import Logger -from plugins.UM3NetworkPrinting.src.Models.BaseModel import BaseModel + +from ..Models.BaseModel import BaseModel +from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus +from ..Models.Http.ClusterPrinterStatus import ClusterPrinterStatus ## The generic type variable used to document the methods below. -from plugins.UM3NetworkPrinting.src.Models.Http.ClusterPrinterStatus import ClusterPrinterStatus - ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel) @@ -53,13 +54,23 @@ class ClusterApiClient: ## Get the print jobs in the cluster. # \param on_finished: The callback in case the response is successful. - def getPrintJobs(self, on_finished: Callable) -> None: + def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: url = f"{self.CLUSTER_API_PREFIX}/print_jobs/" - # reply = self._manager.get(self._createEmptyRequest(url)) - # self._addCallback(reply, on_finished) + reply = self._manager.get(self._createEmptyRequest(url)) + self._addCallback(reply, on_finished, ClusterPrintJobStatus) def requestPrint(self) -> None: - pass + pass # TODO + + ## Move a print job to the top of the queue. + def movePrintJobToTop(self, print_job_uuid: str) -> None: + url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action/move" + self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode()) + + ## Delete a print job from the queue. + def deletePrintJob(self, print_job_uuid: str) -> None: + url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}" + self._manager.deleteResource(self._createEmptyRequest(url)) ## Send a print job action to the cluster. # \param print_job_uuid: The UUID of the print job to perform the action on. @@ -68,7 +79,7 @@ class ClusterApiClient: def doPrintJobAction(self, print_job_uuid: str, action: str, data: Optional[Dict[str, Union[str, int]]] = None ) -> None: url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action/{action}/" - body = json.loads(data).encode() if data else b"" + body = json.dumps(data).encode() if data else b"" self._manager.put(self._createEmptyRequest(url), body) ## We override _createEmptyRequest in order to add the user credentials. diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py similarity index 98% rename from plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py rename to plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 4cbe8b9194..ff57719105 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -20,7 +20,7 @@ from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOut I18N_CATALOG = i18nCatalog("cura") -class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): +class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): activeCameraUrlChanged = pyqtSignal() @@ -88,11 +88,11 @@ class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="sendJobToTop") def sendJobToTop(self, print_job_uuid: str) -> None: - self._cluster_api.doPrintJobAction(print_job_uuid, "move", {"to_position": 0, "list": "queued"}) + self._cluster_api.movePrintJobToTop(print_job_uuid) @pyqtSlot(str, name="deleteJobFromQueue") def deleteJobFromQueue(self, print_job_uuid: str) -> None: - self._cluster_api.doPrintJobAction(print_job_uuid, "delete") + self._cluster_api.deletePrintJob(print_job_uuid) @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index e4f61ba091..a60fbfa664 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -15,9 +15,10 @@ from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice -from plugins.UM3NetworkPrinting.src.Network.ClusterApiClient import ClusterApiClient -from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice -from plugins.UM3NetworkPrinting.src.Network.ManualPrinterRequest import ManualPrinterRequest + +from .ClusterApiClient import ClusterApiClient +from .LocalClusterOutputDevice import LocalClusterOutputDevice +from .ManualPrinterRequest import ManualPrinterRequest ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. @@ -37,7 +38,7 @@ class NetworkOutputDeviceManager: def __init__(self) -> None: # Persistent dict containing the networked clusters. - self._discovered_devices = {} # type: Dict[str, ClusterUM3OutputDevice] + self._discovered_devices = {} # type: Dict[str, LocalClusterOutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() self._zero_conf = None # type: Optional[Zeroconf] @@ -211,7 +212,7 @@ class NetworkOutputDeviceManager: if cluster_size == -1: return - device = ClusterUM3OutputDevice(key, address, properties) + device = LocalClusterOutputDevice(key, address, properties) CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( ip_address=address, diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 8509b3aab3..697ba33a6b 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -1,6 +1,5 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - import json import os from typing import Dict, TYPE_CHECKING, Set, Optional @@ -9,11 +8,12 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from UM.Job import Job from UM.Logger import Logger from cura.CuraApplication import CuraApplication -from plugins.UM3NetworkPrinting.src.Models.ClusterMaterial import ClusterMaterial -from plugins.UM3NetworkPrinting.src.Models.LocalMaterial import LocalMaterial + +from .Models.ClusterMaterial import ClusterMaterial +from .Models.LocalMaterial import LocalMaterial if TYPE_CHECKING: - from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice + from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice ## Asynchronous job to send material profiles to the printer. @@ -21,9 +21,9 @@ if TYPE_CHECKING: # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): - def __init__(self, device: "ClusterUM3OutputDevice") -> None: + def __init__(self, device: "LocalClusterOutputDevice") -> None: super().__init__() - self.device = device # type: ClusterUM3OutputDevice + self.device = device # type: LocalClusterOutputDevice ## Send the request to the printer and register a callback def run(self) -> None: diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 7f7f69a241..fad2fc46f3 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -6,8 +6,8 @@ from cura.CuraApplication import CuraApplication from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin -from plugins.UM3NetworkPrinting.src.Network.NetworkOutputDeviceManager import NetworkOutputDeviceManager +from .Network.NetworkOutputDeviceManager import NetworkOutputDeviceManager from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager @@ -73,16 +73,6 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: self._network_output_device_manager.removeManualDevice(key, address) - # ## Get the last manual device attempt. - # # Used by the DiscoverUM3Action. - # def getLastManualDevice(self) -> str: - # return self._network_output_device_manager.getLastManualDevice() - - # ## Reset the last manual device attempt. - # # Used by the DiscoverUM3Action. - # def resetLastManualDevice(self) -> None: - # self._network_output_device_manager.resetLastManualDevice() - # ## Check if the prerequsites are in place to start the cloud flow # def checkCloudFlowIsPossible(self, cluster: Optional[CloudOutputDevice]) -> None: # Logger.log("d", "Checking if cloud connection is possible...") diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 0298518ba9..b098be629e 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -11,12 +11,12 @@ from cura.CuraApplication import CuraApplication from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from plugins.UM3NetworkPrinting.src.Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus from .Utils import formatTimeCompleted, formatDateCompleted from .ClusterOutputController import ClusterOutputController from .Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus +from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus ## Output device class that forms the basis of Ultimaker networked printer output devices. @@ -211,8 +211,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self.printersChanged.emit() - ## Updates the local list of print jobs with the list received from the cloud. - # \param remote_jobs: The print jobs received from the cloud. + ## Updates the local list of print jobs with the list received from the cluster. + # \param remote_jobs: The print jobs received from the cluster. def _updatePrintJobs(self, remote_jobs: List[ClusterPrintJobStatus]) -> None: # Keep track of the new print jobs to show. @@ -251,6 +251,9 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._updateAssignedPrinter(model, remote_job.printer_uuid) return model + def _onPrintJobStateChanged(self) -> None: + pass + ## Updates the printer assignment for the given print job model. def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: printer = next((p for p in self._printers if printer_uuid == p.key), None) diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py index c1de91cf7c..442c422f95 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py @@ -6,12 +6,14 @@ from unittest import TestCase from unittest.mock import patch, MagicMock from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot + from ...src.Cloud import CloudApiClient -from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse -from plugins.UM3NetworkPrinting.src.Models.CloudClusterStatus import CloudClusterStatus -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobResponse import CloudPrintJobResponse -from plugins.UM3NetworkPrinting.src.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from plugins.UM3NetworkPrinting.src.Models.CloudError import CloudError +from ...src.Models.Http.CloudClusterResponse import CloudClusterResponse +from ...src.Models.Http.CloudClusterStatus import CloudClusterStatus +from ...src.Models.Http.CloudPrintJobResponse import CloudPrintJobResponse +from ...src.Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest +from ...src.Models.Http.CloudError import CloudError + from .Fixtures import readFixture, parseFixture from .NetworkManagerMock import NetworkManagerMock diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py index 46a2414005..07c44753c4 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py @@ -9,7 +9,7 @@ from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from ...src.Cloud import CloudApiClient from ...src.Cloud.CloudOutputDevice import CloudOutputDevice -from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse +from ...src.Models.Http.CloudClusterResponse import CloudClusterResponse from .Fixtures import readFixture, parseFixture from .NetworkManagerMock import NetworkManagerMock diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py index b0d1c83f8d..105f42c798 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py @@ -5,10 +5,12 @@ from unittest.mock import patch, MagicMock from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot + from ...src.Cloud import CloudApiClient from ...src.Cloud import CloudOutputDeviceManager -from plugins.UM3NetworkPrinting.src.Models.CloudClusterResponse import CloudClusterResponse +from ...src.Models.Http.CloudClusterResponse import CloudClusterResponse from .Fixtures import parseFixture, readFixture + from .NetworkManagerMock import NetworkManagerMock, FakeSignal From 1aa70748aff3eeaac5b6f41dd07172069d3b67b7 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 17:24:10 +0200 Subject: [PATCH 08/52] Consistent naming, some bug fixes --- .../Models/DiscoveredPrintersModel.py | 2 +- cura/Settings/GlobalStack.py | 2 +- .../src/Cloud/CloudOutputDeviceManager.py | 59 ++++---- ...OutputDevice.py => NetworkOutputDevice.py} | 2 +- .../src/Network/NetworkOutputDeviceManager.py | 131 ++++++------------ .../UM3NetworkPrinting/src/SendMaterialJob.py | 6 +- 6 files changed, 70 insertions(+), 132 deletions(-) rename plugins/UM3NetworkPrinting/src/Network/{LocalClusterOutputDevice.py => NetworkOutputDevice.py} (99%) diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index 33e0b7a4d9..a1b68ee1ae 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject): def readableMachineType(self) -> str: from cura.CuraApplication import CuraApplication machine_manager = CuraApplication.getInstance().getMachineManager() - # In LocalClusterOutputDevice, when it updates a printer information, it updates the machine type using the field + # In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field # "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string # like "Ultimaker 3". The code below handles this case. if self._hasHumanReadableMachineTypeName(self._machine_type): diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index 3502088e2d..7efc263180 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -118,7 +118,7 @@ class GlobalStack(CuraContainerStack): ## \sa configuredConnectionTypes def removeConfiguredConnectionType(self, connection_type: int) -> None: configured_connection_types = self.configuredConnectionTypes - if connection_type in self.configured_connection_types: + if connection_type in configured_connection_types: # Store the values as a string. configured_connection_types.remove(connection_type) self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types])) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index e1e54c2991..2cd3f5a534 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -6,7 +6,6 @@ from PyQt5.QtCore import QTimer from UM import i18nCatalog from UM.Logger import Logger -from UM.Message import Message from UM.Signal import Signal from cura.API import Account from cura.CuraApplication import CuraApplication @@ -54,13 +53,32 @@ class CloudOutputDeviceManager: self._running = False + ## Starts running the cloud output device manager, thus periodically requesting cloud data. + def start(self): + if self._running: + return + self._account.loginStateChanged.connect(self._onLoginStateChanged) + # When switching machines we check if we have to activate a remote cluster. + self._application.globalContainerStackChanged.connect(self._connectToActiveMachine) + self._update_timer.timeout.connect(self._getRemoteClusters) + self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn) + + ## Stops running the cloud output device manager. + def stop(self): + if not self._running: + return + self._account.loginStateChanged.disconnect(self._onLoginStateChanged) + # When switching machines we check if we have to activate a remote cluster. + self._application.globalContainerStackChanged.disconnect(self._connectToActiveMachine) + self._update_timer.timeout.disconnect(self._getRemoteClusters) + self._onLoginStateChanged(is_logged_in = False) + ## Force refreshing connections. def refreshConnections(self) -> None: - pass + self._connectToActiveMachine() ## Called when the uses logs in or out def _onLoginStateChanged(self, is_logged_in: bool) -> None: - Logger.log("d", "Log in state changed to %s", is_logged_in) if is_logged_in: if not self._update_timer.isActive(): self._update_timer.start() @@ -74,18 +92,13 @@ class CloudOutputDeviceManager: ## Gets all remote clusters from the API. def _getRemoteClusters(self) -> None: - Logger.log("d", "Retrieving remote clusters") self._api.getClusters(self._onGetRemoteClustersFinished) ## Callback for when the request for getting the clusters. is finished. def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] - removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters) - Logger.log("d", "Parsed remote clusters to %s", [cluster.toDict() for cluster in online_clusters.values()]) - Logger.log("d", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates)) - # Remove output devices that are gone for device in removed_devices: if device.isConnected(): @@ -136,12 +149,7 @@ class CloudOutputDeviceManager: # 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) + self._connectToActiveMachine() ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: @@ -182,31 +190,12 @@ class CloudOutputDeviceManager: ## Connects to an output device and makes sure it is registered in the output device manager. def _connectToOutputDevice(self, device: CloudOutputDevice, active_machine: GlobalStack) -> None: device.connect() - self._output_device_manager.addOutputDevice(device) + active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) active_machine.addConfiguredConnectionType(device.connectionType.value) + self._output_device_manager.addOutputDevice(device) ## Handles an API error received from the cloud. # \param errors: The errors received @staticmethod def _onApiError(errors: List[CloudError] = None) -> None: Logger.log("w", str(errors)) - - ## Starts running the cloud output device manager, thus periodically requesting cloud data. - def start(self): - if self._running: - return - self._account.loginStateChanged.connect(self._onLoginStateChanged) - # When switching machines we check if we have to activate a remote cluster. - self._application.globalContainerStackChanged.connect(self._connectToActiveMachine) - self._update_timer.timeout.connect(self._getRemoteClusters) - self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn) - - ## Stops running the cloud output device manager. - def stop(self): - if not self._running: - return - self._account.loginStateChanged.disconnect(self._onLoginStateChanged) - # When switching machines we check if we have to activate a remote cluster. - self._application.globalContainerStackChanged.disconnect(self._connectToActiveMachine) - self._update_timer.timeout.disconnect(self._getRemoteClusters) - self._onLoginStateChanged(is_logged_in = False) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py similarity index 99% rename from plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py rename to plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index ff57719105..523bb8a6af 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -20,7 +20,7 @@ from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOut I18N_CATALOG = i18nCatalog("cura") -class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): +class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): activeCameraUrlChanged = pyqtSignal() diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index a60fbfa664..9f33439f18 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -15,9 +15,10 @@ from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice +from cura.Settings.GlobalStack import GlobalStack from .ClusterApiClient import ClusterApiClient -from .LocalClusterOutputDevice import LocalClusterOutputDevice +from .NetworkOutputDevice import NetworkOutputDevice from .ManualPrinterRequest import ManualPrinterRequest @@ -38,9 +39,10 @@ class NetworkOutputDeviceManager: def __init__(self) -> None: # Persistent dict containing the networked clusters. - self._discovered_devices = {} # type: Dict[str, LocalClusterOutputDevice] + self._discovered_devices = {} # type: Dict[str, NetworkOutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() + # TODO: move zeroconf stuff to own class self._zero_conf = None # type: Optional[Zeroconf] self._zero_conf_browser = None # type: Optional[ServiceBrowser] self._service_changed_request_queue = None # type: Optional[Queue] @@ -55,30 +57,6 @@ class NetworkOutputDeviceManager: self.addedNetworkCluster.connect(self._onAddDevice) self.removedNetworkCluster.connect(self._onRemoveDevice) - ## Force reset all network device connections. - def refreshConnections(self): - active_machine = CuraApplication.getInstance().getGlobalContainerStack() - if not active_machine: - return - - um_network_key = active_machine.getMetaDataEntry("um_network_key") - for key in self._discovered_devices: - if key == um_network_key: - if not self._discovered_devices[key].isConnected(): - Logger.log("d", "Attempting to connect with [%s]" % key) - # It should already be set, but if it actually connects we know for sure it's supported! - active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value) - self._discovered_devices[key].connect() - self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged) - else: - self._onDeviceConnectionStateChanged(key) - else: - if self._discovered_devices[key].isConnected(): - Logger.log("d", "Attempting to close connection with [%s]" % key) - self._discovered_devices[key].close() - self._discovered_devices[key].connectionStateChanged.disconnect( - self._onDeviceConnectionStateChanged) - ## Start the network discovery. def start(self): # The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. @@ -145,7 +123,6 @@ class NetworkOutputDeviceManager: if not address: address = self._discovered_devices[key].ipAddress self._onRemoveDevice(key) - # TODO: self.resetLastManualDevice() if address in self._manual_instances: manual_printer_request = self._manual_instances.pop(address) @@ -155,6 +132,33 @@ class NetworkOutputDeviceManager: if manual_printer_request.callback is not None: CuraApplication.getInstance().callLater(manual_printer_request.callback, False, address) + ## Force reset all network device connections. + def refreshConnections(self): + self._connectToActiveMachine() + + ## Callback for when the active machine was changed by the user or a new remote cluster was found. + def _connectToActiveMachine(self): + active_machine = CuraApplication.getInstance().getGlobalContainerStack() + if not active_machine: + return + + for device_id in self._discovered_devices: + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device_id) + + stored_network_key = active_machine.getMetaDataEntry("um_network_key") + if stored_network_key in self._discovered_devices: + device = self._discovered_devices[stored_network_key] + self._connectToOutputDevice(device, active_machine) + + ## Add a device to the current active machine. + @staticmethod + def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None: + device.connect() + active_machine.setMetaDataEntry("um_network_key", device.key) + active_machine.setMetaDataEntry("group_name", device.name) + active_machine.addConfiguredConnectionType(device.connectionType.value) + CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) + ## Handles an API error received from the cloud. # \param errors: The errors received def _onApiError(self, errors) -> None: @@ -212,7 +216,7 @@ class NetworkOutputDeviceManager: if cluster_size == -1: return - device = LocalClusterOutputDevice(key, address, properties) + device = NetworkOutputDevice(key, address, properties) CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( ip_address=address, @@ -225,28 +229,17 @@ class NetworkOutputDeviceManager: self._discovered_devices[device.getId()] = device self.discoveredDevicesChanged.emit() - - global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() - if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"): - # Ensure that the configured connection type is set. - CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) - global_container_stack.addConfiguredConnectionType(device.connectionType.value) - device.connect() - device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged) + self._connectToActiveMachine() ## Remove a device. def _onRemoveDevice(self, device_id: str) -> None: device = self._discovered_devices.pop(device_id, None) - if device: - if device.isConnected(): - device.disconnect() - try: - device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) - except TypeError: - # Disconnect already happened. - pass - CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) - self.discoveredDevicesChanged.emit() + if not device: + return + if device.isConnected(): + device.disconnect() + CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) + self.discoveredDevicesChanged.emit() ## Appends a service changed request so later the handling thread will pick it up and processes it. def _appendServiceChangedRequest(self, zeroconf: Zeroconf, service_type, name: str, @@ -285,20 +278,6 @@ class NetworkOutputDeviceManager: for request in reschedule_requests: self._service_changed_request_queue.put(request) - ## Callback handler for when the connection state of a networked device has changed. - def _onDeviceConnectionStateChanged(self, key: str) -> None: - if key not in self._discovered_devices: - return - - if self._discovered_devices[key].isConnected(): - um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key") - if key != um_network_key: - return - CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(self._discovered_devices[key]) - # TODO: self.checkCloudFlowIsPossible(None) - else: - CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(key) - ## Handler for zeroConf detection. # Return True or False indicating if the process succeeded. # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. @@ -347,48 +326,18 @@ class NetworkOutputDeviceManager: self.removedNetworkCluster.emit(str(name)) return True - def _associateActiveMachineWithPrinterDevice(self, printer_device: Optional[PrinterOutputDevice]) -> None: - if not printer_device: - return - - Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key) - - machine_manager = CuraApplication.getInstance().getMachineManager() - global_container_stack = machine_manager.activeMachine - if not global_container_stack: - return - - for machine in machine_manager.getMachinesInGroup(global_container_stack.getMetaDataEntry("group_id")): - machine.setMetaDataEntry("um_network_key", printer_device.key) - machine.setMetaDataEntry("group_name", printer_device.name) - - # Delete old authentication data. - Logger.log("d", "Removing old authentication id %s for device %s", - global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key) - - machine.removeMetaDataEntry("network_authentication_id") - machine.removeMetaDataEntry("network_authentication_key") - - # Ensure that these containers do know that they are configured for network connection - machine.addConfiguredConnectionType(printer_device.connectionType.value) - ## Create a machine instance based on the discovered network printer. def _createMachineFromDiscoveredPrinter(self, key: str) -> None: discovered_device = self._discovered_devices.get(key) if discovered_device is None: Logger.log("e", "Could not find discovered device with key [%s]", key) return - group_name = discovered_device.getProperty("name") machine_type_id = discovered_device.getProperty("printer_type") Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]", key, group_name, machine_type_id) - CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) - # connect the new machine to that network printer - self._associateActiveMachineWithPrinterDevice(discovered_device) - # ensure that the connection states are refreshed. - self.refreshConnections() + self._connectToActiveMachine() ## Load the user-configured manual devices from Cura preferences. def _getStoredManualInstances(self) -> Dict[str, ManualPrinterRequest]: diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 697ba33a6b..50705efa8e 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -13,7 +13,7 @@ from .Models.ClusterMaterial import ClusterMaterial from .Models.LocalMaterial import LocalMaterial if TYPE_CHECKING: - from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice + from .Network.NetworkOutputDevice import NetworkOutputDevice ## Asynchronous job to send material profiles to the printer. @@ -21,9 +21,9 @@ if TYPE_CHECKING: # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): - def __init__(self, device: "LocalClusterOutputDevice") -> None: + def __init__(self, device: "NetworkOutputDevice") -> None: super().__init__() - self.device = device # type: LocalClusterOutputDevice + self.device = device # type: NetworkOutputDevice ## Send the request to the printer and register a callback def run(self) -> None: From 93146610f7277c17f018cf4161489dac912cf1d0 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 17:31:05 +0200 Subject: [PATCH 09/52] Fix local pause, resume and abort --- .../src/Network/ClusterApiClient.py | 18 +++++++++--------- .../src/Network/NetworkOutputDevice.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index e6a816abcd..3e3dbed46d 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -72,15 +72,15 @@ class ClusterApiClient: url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}" self._manager.deleteResource(self._createEmptyRequest(url)) - ## Send a print job action to the cluster. - # \param print_job_uuid: The UUID of the print job to perform the action on. - # \param action: The action to perform. - # \param data: The optional data to send along, used for 'move' and 'duplicate'. - def doPrintJobAction(self, print_job_uuid: str, action: str, data: Optional[Dict[str, Union[str, int]]] = None - ) -> None: - url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action/{action}/" - body = json.dumps(data).encode() if data else b"" - self._manager.put(self._createEmptyRequest(url), body) + ## Set the state of a print job. + def setPrintJobState(self, print_job_uuid: str, state: str) -> None: + url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action" + # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. + action = "print" if state == "resume" else state + self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) + + def forcePrintJob(self, print_job_uuid: str) -> None: + pass # TODO ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 523bb8a6af..0f516a8aeb 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -96,13 +96,13 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: - self._cluster_api.doPrintJobAction(print_job_uuid, "force") + self._cluster_api.forcePrintJob(print_job_uuid) ## Set the remote print job state. # \param print_job_uuid: The UUID of the print job to set the state for. # \param action: The action to undertake ('pause', 'resume', 'abort'). def setJobState(self, print_job_uuid: str, action: str) -> None: - self._cluster_api.doPrintJobAction(print_job_uuid, action) + self._cluster_api.setPrintJobState(print_job_uuid, action) ## Handle network errors. @staticmethod From 14808c5de1138af8cea1a53d8a13c92462e86bd8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 17:34:31 +0200 Subject: [PATCH 10/52] Don't cache application instance --- .../src/Cloud/CloudOutputDeviceManager.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 2cd3f5a534..0f33e44a1e 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -39,11 +39,7 @@ class CloudOutputDeviceManager: def __init__(self) -> None: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] - - self._application = CuraApplication.getInstance() - self._output_device_manager = self._application.getOutputDeviceManager() - - self._account = self._application.getCuraAPI().account # type: Account + self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._api = CloudApiClient(self._account, self._onApiError) # Create a timer to update the remote cluster list @@ -58,8 +54,6 @@ class CloudOutputDeviceManager: if self._running: return self._account.loginStateChanged.connect(self._onLoginStateChanged) - # When switching machines we check if we have to activate a remote cluster. - self._application.globalContainerStackChanged.connect(self._connectToActiveMachine) self._update_timer.timeout.connect(self._getRemoteClusters) self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn) @@ -68,8 +62,6 @@ class CloudOutputDeviceManager: if not self._running: return self._account.loginStateChanged.disconnect(self._onLoginStateChanged) - # When switching machines we check if we have to activate a remote cluster. - self._application.globalContainerStackChanged.disconnect(self._connectToActiveMachine) self._update_timer.timeout.disconnect(self._getRemoteClusters) self._onLoginStateChanged(is_logged_in = False) @@ -104,8 +96,8 @@ class CloudOutputDeviceManager: if device.isConnected(): device.disconnect() device.close() - self._output_device_manager.removeOutputDevice(device.key) - self._application.getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key) + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) + CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key) self.removedCloudCluster.emit(device) del self._remote_clusters[device.key] @@ -114,7 +106,7 @@ class CloudOutputDeviceManager: for cluster in added_clusters: device = CloudOutputDevice(self._api, cluster) self._remote_clusters[cluster.cluster_id] = device - self._application.getDiscoveredPrintersModel().addDiscoveredPrinter( + CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( device.key, device.key, cluster.friendly_name, @@ -127,7 +119,7 @@ class CloudOutputDeviceManager: # Update the output devices for device, cluster in updates: device.clusterData = cluster - self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter( + CuraApplication.getInstance().getDiscoveredPrintersModel().updateDiscoveredPrinter( device.key, cluster.friendly_name, device.printerType, @@ -148,7 +140,7 @@ class CloudOutputDeviceManager: key, group_name, machine_type_id) # The newly added machine is automatically activated. - self._application.getMachineManager().addMachine(machine_type_id, group_name) + CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) self._connectToActiveMachine() ## Callback for when the active machine was changed by the user or a new remote cluster was found. @@ -161,7 +153,7 @@ class CloudOutputDeviceManager: # This is needed because when we switch machines we can only leave # output devices that are meant for that machine. for stored_cluster_id in self._remote_clusters: - self._output_device_manager.removeOutputDevice(stored_cluster_id) + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(stored_cluster_id) # Check if the stored cluster_id for the active machine is in our list of remote clusters. stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) @@ -192,7 +184,7 @@ class CloudOutputDeviceManager: device.connect() active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) active_machine.addConfiguredConnectionType(device.connectionType.value) - self._output_device_manager.addOutputDevice(device) + CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) ## Handles an API error received from the cloud. # \param errors: The errors received From 6d9b6668e2e08e02480d5d450ad5fe49abf9a8ff Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 17:38:07 +0200 Subject: [PATCH 11/52] Add TODO --- .../UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py | 5 +++-- .../src/Network/NetworkOutputDeviceManager.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 0f33e44a1e..c09305df59 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -47,6 +47,7 @@ class CloudOutputDeviceManager: self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000)) self._update_timer.setSingleShot(False) + # Ensure we don't start twice. self._running = False ## Starts running the cloud output device manager, thus periodically requesting cloud data. @@ -55,7 +56,7 @@ class CloudOutputDeviceManager: return self._account.loginStateChanged.connect(self._onLoginStateChanged) self._update_timer.timeout.connect(self._getRemoteClusters) - self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn) + self._onLoginStateChanged(is_logged_in=self._account.isLoggedIn) ## Stops running the cloud output device manager. def stop(self): @@ -63,7 +64,7 @@ class CloudOutputDeviceManager: return self._account.loginStateChanged.disconnect(self._onLoginStateChanged) self._update_timer.timeout.disconnect(self._getRemoteClusters) - self._onLoginStateChanged(is_logged_in = False) + self._onLoginStateChanged(is_logged_in=False) ## Force refreshing connections. def refreshConnections(self) -> None: diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 9f33439f18..0633f8e0bc 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -42,13 +42,14 @@ class NetworkOutputDeviceManager: self._discovered_devices = {} # type: Dict[str, NetworkOutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() - # TODO: move zeroconf stuff to own class + # TODO: move zeroconf stuff to own class? self._zero_conf = None # type: Optional[Zeroconf] self._zero_conf_browser = None # type: Optional[ServiceBrowser] self._service_changed_request_queue = None # type: Optional[Queue] self._service_changed_request_event = None # type: Optional[Event] self._service_changed_request_thread = None # type: Optional[Thread] + # TODO: move manual device stuff to own class? # Persistent dict containing manually connected clusters. self._manual_instances = {} # type: Dict[str, ManualPrinterRequest] self._last_manual_entry_key = None # type: Optional[str] From 67045b34b5116025205803bf0b18d22aa46e89be Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 17:39:27 +0200 Subject: [PATCH 12/52] Add some more explanation --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index fad2fc46f3..18ee65b66f 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -26,6 +26,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._cloud_output_device_manager = CloudOutputDeviceManager() # Refresh network connections when another machine was selected in Cura. + # This ensures no output devices are still connected that do not belong to the new active machine. CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) # TODO: re-write cloud messaging From 5b206496d04d4298304650c65f555a59e2fbcc45 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 17:41:27 +0200 Subject: [PATCH 13/52] Formatting new line --- .../src/Models/Http/ClusterPrintCoreConfiguration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index c27b1691d3..ffc80c1810 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -4,6 +4,7 @@ from typing import Union, Dict, Optional, Any from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel + from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMaterial from ..BaseModel import BaseModel From 8360b5b448032a3ec52d95dc988c0d91108c2b82 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 19:48:57 +0200 Subject: [PATCH 14/52] Simply manual device checking --- .../src/Cloud/CloudOutputDevice.py | 3 +- .../src/Cloud/CloudOutputDeviceManager.py | 76 +++++++------- .../src/Network/ManualPrinterRequest.py | 13 --- .../src/Network/NetworkOutputDeviceManager.py | 99 +++++++++---------- plugins/UM3NetworkPrinting/src/Utils.py | 26 ----- 5 files changed, 82 insertions(+), 135 deletions(-) delete mode 100644 plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index b337da4ef5..0147851343 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -112,6 +112,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Disconnects the device def disconnect(self) -> None: + if not self.isConnected(): + return super().disconnect() Logger.log("i", "Disconnected from cluster %s", self.key) CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange) @@ -201,7 +203,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): if self._received_printers != status.printers: self._received_printers = status.printers self._updatePrinters(status.printers) - if status.print_jobs != self._received_print_jobs: self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index c09305df59..e6ed31219c 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -15,7 +15,6 @@ from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudError import CloudError -from ..Utils import findChanges ## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. @@ -33,8 +32,8 @@ class CloudOutputDeviceManager: # The translation catalog for this device. I18N_CATALOG = i18nCatalog("cura") - addedCloudCluster = Signal() - removedCloudCluster = Signal() + # Signal emitted when the list of discovered devices changed. + discoveredDevicesChanged = Signal() def __init__(self) -> None: # Persistent dict containing the remote clusters for the authenticated user. @@ -79,7 +78,6 @@ class CloudOutputDeviceManager: else: if self._update_timer.isActive(): self._update_timer.stop() - # Notify that all clusters have disappeared self._onGetRemoteClustersFinished([]) @@ -87,45 +85,39 @@ class CloudOutputDeviceManager: def _getRemoteClusters(self) -> None: self._api.getClusters(self._onGetRemoteClustersFinished) - ## Callback for when the request for getting the clusters. is finished. + ## Callback for when the request for getting the clusters is finished. def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: + + # Filter on clusters that are currently online. online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] - removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters) - # Remove output devices that are gone + # Keep track of the new cloud clusters to show. + # We create a new list instead of changing the existing one to prevent issues with ordering. + new_devices = {} # type: Dict[str, CloudOutputDevice] + + # Get the discovery mechanism of Cura. + discovery = CuraApplication.getInstance().getDiscoveredPrintersModel() + + # Check which devices need to be created or updated. + for device_id, cluster_data in online_clusters.items(): + device = next(iter(device for device in self._remote_clusters.values() if device.key == device_id), None) + if not device: + device = CloudOutputDevice(self._api, cluster_data) + discovery.addDiscoveredPrinter(device.key, device.key, cluster_data.friendly_name, + self._createMachineFromDiscoveredPrinter, device.printerType, device) + else: + discovery.updateDiscoveredPrinter(device.key, cluster_data.friendly_name, device.printerType) + new_devices[device.key] = device + + # Remove output devices that disappeared. + remote_cluster_keys = self._remote_clusters.keys() + removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in remote_cluster_keys] for device in removed_devices: - if device.isConnected(): - device.disconnect() - device.close() CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) - CuraApplication.getInstance().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 cluster in added_clusters: - device = CloudOutputDevice(self._api, cluster) - self._remote_clusters[cluster.cluster_id] = device - CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( - device.key, - 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 - CuraApplication.getInstance().getDiscoveredPrintersModel().updateDiscoveredPrinter( - device.key, - cluster.friendly_name, - device.printerType, - ) + discovery.removeDiscoveredPrinter(device.key) + self._remote_clusters = new_devices + self.discoveredDevicesChanged.emit() self._connectToActiveMachine() def _createMachineFromDiscoveredPrinter(self, key: str) -> None: @@ -151,10 +143,9 @@ class CloudOutputDeviceManager: return # Remove all output devices that we have registered. - # This is needed because when we switch machines we can only leave - # output devices that are meant for that machine. - for stored_cluster_id in self._remote_clusters: - CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(stored_cluster_id) + # This is needed because when we switch we can only leave output devices that are meant for that machine. + for device_id in self._remote_clusters: + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device_id) # Check if the stored cluster_id for the active machine is in our list of remote clusters. stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) @@ -191,4 +182,5 @@ class CloudOutputDeviceManager: # \param errors: The errors received @staticmethod def _onApiError(errors: List[CloudError] = None) -> None: - Logger.log("w", str(errors)) + for error in errors: + Logger.log("w", str(error.toDict())) diff --git a/plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py b/plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py deleted file mode 100644 index 3a5bfb2651..0000000000 --- a/plugins/UM3NetworkPrinting/src/Network/ManualPrinterRequest.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional, Callable - - -## Represents a request for adding a manual printer. It has the following fields: -# - address: The string of the (IP) address of the manual printer -# - callback: (Optional) Once the HTTP request to the printer to get printer information is done, whether successful -# or not, this callback will be invoked to notify about the result. The callback must have a signature of -# func(success: bool, address: str) -> None -class ManualPrinterRequest: - - def __init__(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: - self.address = address - self.callback = callback diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 0633f8e0bc..fb660610e3 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -9,7 +9,6 @@ from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo from UM import i18nCatalog from UM.Logger import Logger -from UM.Message import Message from UM.Signal import Signal from UM.Version import Version @@ -19,7 +18,6 @@ from cura.Settings.GlobalStack import GlobalStack from .ClusterApiClient import ClusterApiClient from .NetworkOutputDevice import NetworkOutputDevice -from .ManualPrinterRequest import ManualPrinterRequest ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. @@ -32,7 +30,10 @@ class NetworkOutputDeviceManager: # The translation catalog for this device. I18N_CATALOG = i18nCatalog("cura") + # Signal emitted when the list of discovered devices changed. discoveredDevicesChanged = Signal() + + # Signals emitted when new services were discovered or removed on the network. addedNetworkCluster = Signal() removedNetworkCluster = Signal() @@ -51,8 +52,7 @@ class NetworkOutputDeviceManager: # TODO: move manual device stuff to own class? # Persistent dict containing manually connected clusters. - self._manual_instances = {} # type: Dict[str, ManualPrinterRequest] - self._last_manual_entry_key = None # type: Optional[str] + self._manual_instances = {} # type: Dict[str, Callable] # Hook up the signals for discovery. self.addedNetworkCluster.connect(self._onAddDevice) @@ -78,8 +78,7 @@ class NetworkOutputDeviceManager: # Load all manual devices. self._manual_instances = self._getStoredManualInstances() for address in self._manual_instances: - if address: - self.addManualDevice(address) + self.addManualDevice(address) ## Stop network discovery and clean up discovered devices. def stop(self): @@ -97,7 +96,7 @@ class NetworkOutputDeviceManager: ## Add a networked printer manually by address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: - self._manual_instances[address] = ManualPrinterRequest(address, callback=callback) + self._manual_instances[address] = callback new_manual_devices = ",".join(self._manual_instances.keys()) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) @@ -111,7 +110,6 @@ class NetworkOutputDeviceManager: b"temporary": b"true" }) - self._last_manual_entry_key = key response_callback = lambda status_code, response: self._onCheckManualDeviceResponse(status_code, address) self._checkManualDevice(address, response_callback) @@ -126,12 +124,12 @@ class NetworkOutputDeviceManager: self._onRemoveDevice(key) if address in self._manual_instances: - manual_printer_request = self._manual_instances.pop(address) + manual_instance_callback = self._manual_instances.pop(address) new_manual_devices = ",".join(self._manual_instances.keys()) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) - if manual_printer_request.callback is not None: - CuraApplication.getInstance().callLater(manual_printer_request.callback, False, address) + if manual_instance_callback: + CuraApplication.getInstance().callLater(manual_instance_callback, False, address) ## Force reset all network device connections. def refreshConnections(self): @@ -143,13 +141,17 @@ class NetworkOutputDeviceManager: if not active_machine: return + # Remove all output devices that we have registered. + # This is needed because when we switch we can only leave output devices that are meant for that machine. for device_id in self._discovered_devices: CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device_id) + # Check if the stored network key for the active machine is in our list of discovered devices. stored_network_key = active_machine.getMetaDataEntry("um_network_key") if stored_network_key in self._discovered_devices: device = self._discovered_devices[stored_network_key] self._connectToOutputDevice(device, active_machine) + Logger.log("d", "Device connected by metadata network key %s", stored_network_key) ## Add a device to the current active machine. @staticmethod @@ -160,17 +162,6 @@ class NetworkOutputDeviceManager: active_machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) - ## Handles an API error received from the cloud. - # \param errors: The errors received - def _onApiError(self, errors) -> None: - Logger.log("w", str(errors)) - message = Message( - text=self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the printer."), - title=self.I18N_CATALOG.i18nc("@info:title", "Error"), - lifetime=10 - ) - message.show() - ## Checks if a networked printer exists at the given address. # If the printer responds it will replace the preliminary printer created from the stored manual instances. def _checkManualDevice(self, address: str, on_finished: Callable) -> None: @@ -181,12 +172,12 @@ class NetworkOutputDeviceManager: def _onCheckManualDeviceResponse(self, status_code: int, address: str) -> None: Logger.log("d", "manual device check response: {} {}".format(status_code, address)) if address in self._manual_instances: - callback = self._manual_instances[address].callback - if callback: + callback = self._manual_instances[address] + if callback is not None: CuraApplication.getInstance().callLater(callback, status_code == 200, address) - ## Returns a dict of printer BOM numbers to machine types. - # These numbers are available in the machine definition already so we just search for them here. + ## Returns a dict of printer BOM numbers to machine types. + # These numbers are available in the machine definition already so we just search for them here. @staticmethod def _getPrinterTypeIdentifiers() -> Dict[str, str]: container_registry = CuraApplication.getInstance().getContainerRegistry() @@ -218,16 +209,14 @@ class NetworkOutputDeviceManager: return device = NetworkOutputDevice(key, address, properties) - CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( ip_address=address, key=device.getId(), - name=properties[b"name"].decode("utf-8"), + name=device.getName(), create_callback=self._createMachineFromDiscoveredPrinter, - machine_type=properties[b"printer_type"].decode("utf-8"), + machine_type=device.printerType, device=device ) - self._discovered_devices[device.getId()] = device self.discoveredDevicesChanged.emit() self._connectToActiveMachine() @@ -237,11 +226,35 @@ class NetworkOutputDeviceManager: device = self._discovered_devices.pop(device_id, None) if not device: return - if device.isConnected(): - device.disconnect() CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) self.discoveredDevicesChanged.emit() + ## Create a machine instance based on the discovered network printer. + def _createMachineFromDiscoveredPrinter(self, key: str) -> None: + discovered_device = self._discovered_devices.get(key) + if discovered_device is None: + Logger.log("e", "Could not find discovered device with key [%s]", key) + return + group_name = discovered_device.getProperty("name") + machine_type_id = discovered_device.getProperty("printer_type") + Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]", + key, group_name, machine_type_id) + CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) + self._connectToActiveMachine() + + ## Load the user-configured manual devices from Cura preferences. + def _getStoredManualInstances(self) -> Dict[str, Optional[Callable]]: + preferences = CuraApplication.getInstance().getPreferences() + preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") + manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") + return {address: None for address in manual_instances} + + ## Handles an API error received from the cloud. + # \param errors: The errors received + @staticmethod + def _onApiError(errors) -> None: + Logger.log("w", str(errors)) + ## Appends a service changed request so later the handling thread will pick it up and processes it. def _appendServiceChangedRequest(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None: @@ -323,26 +336,6 @@ class NetworkOutputDeviceManager: ## Handler for when a ZeroConf service was removed. def _onServiceRemoved(self, name: str) -> bool: - Logger.log("d", "Bonjour service removed: %s" % name) + Logger.log("d", "ZeroConf service removed: %s" % name) self.removedNetworkCluster.emit(str(name)) return True - - ## Create a machine instance based on the discovered network printer. - def _createMachineFromDiscoveredPrinter(self, key: str) -> None: - discovered_device = self._discovered_devices.get(key) - if discovered_device is None: - Logger.log("e", "Could not find discovered device with key [%s]", key) - return - group_name = discovered_device.getProperty("name") - machine_type_id = discovered_device.getProperty("printer_type") - Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]", - key, group_name, machine_type_id) - CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) - self._connectToActiveMachine() - - ## Load the user-configured manual devices from Cura preferences. - def _getStoredManualInstances(self) -> Dict[str, ManualPrinterRequest]: - preferences = CuraApplication.getInstance().getPreferences() - preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") - manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") - return {address: ManualPrinterRequest(address) for address in manual_instances} diff --git a/plugins/UM3NetworkPrinting/src/Utils.py b/plugins/UM3NetworkPrinting/src/Utils.py index 5136e0e7db..410dd27d1d 100644 --- a/plugins/UM3NetworkPrinting/src/Utils.py +++ b/plugins/UM3NetworkPrinting/src/Utils.py @@ -1,33 +1,7 @@ from datetime import datetime, timedelta -from typing import TypeVar, Dict, Tuple, List from UM import i18nCatalog -T = TypeVar("T") -U = TypeVar("U") - - -## Splits the given dictionaries into three lists (in a tuple): -# - `removed`: Items that were in the first argument but removed in the second one. -# - `added`: Items that were not in the first argument but were included in the second one. -# - `updated`: Items that were in both dictionaries. Both values are given in a tuple. -# \param previous: The previous items -# \param received: The received items -# \return: The tuple (removed, added, updated) as explained above. -def findChanges(previous: Dict[str, T], received: Dict[str, U]) -> Tuple[List[T], List[U], List[Tuple[T, U]]]: - previous_ids = set(previous) - received_ids = set(received) - - removed_ids = previous_ids.difference(received_ids) - new_ids = received_ids.difference(previous_ids) - updated_ids = received_ids.intersection(previous_ids) - - removed = [previous[removed_id] for removed_id in removed_ids] - added = [received[new_id] for new_id in new_ids] - updated = [(previous[updated_id], received[updated_id]) for updated_id in updated_ids] - - return removed, added, updated - def formatTimeCompleted(seconds_remaining: int) -> str: completed = datetime.now() + timedelta(seconds=seconds_remaining) From 4b71b45aa3a01c6ba9c660b1029cae0fe5f5071b Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 21:27:28 +0200 Subject: [PATCH 15/52] Fix for initial connection --- .../src/Cloud/CloudOutputDeviceManager.py | 13 +++++-------- .../src/Network/NetworkOutputDeviceManager.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index e6ed31219c..b6964c7a9c 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -126,15 +126,12 @@ class CloudOutputDeviceManager: 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. - CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) - self._connectToActiveMachine() + CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, + device.clusterData.friendly_name) + active_machine = CuraApplication.getInstance().getGlobalContainerStack() + if active_machine: + 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/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index fb660610e3..5b97bc90f2 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -231,16 +231,16 @@ class NetworkOutputDeviceManager: ## Create a machine instance based on the discovered network printer. def _createMachineFromDiscoveredPrinter(self, key: str) -> None: - discovered_device = self._discovered_devices.get(key) - if discovered_device is None: + device = self._discovered_devices.get(key) + if device is None: Logger.log("e", "Could not find discovered device with key [%s]", key) return - group_name = discovered_device.getProperty("name") - machine_type_id = discovered_device.getProperty("printer_type") - Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]", - key, group_name, machine_type_id) - CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name) - self._connectToActiveMachine() + + # The newly added machine is automatically activated. + CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name) + active_machine = CuraApplication.getInstance().getGlobalContainerStack() + if active_machine: + self._connectToOutputDevice(device, active_machine) ## Load the user-configured manual devices from Cura preferences. def _getStoredManualInstances(self) -> Dict[str, Optional[Callable]]: From 25fde1e0c4077ea64e6b9cb3d261fba71ddc761e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 21:42:52 +0200 Subject: [PATCH 16/52] Extract zeroconf into separate class --- .../src/Network/NetworkOutputDeviceManager.py | 137 ++---------------- .../src/Network/ZeroConfClient.py | 136 +++++++++++++++++ 2 files changed, 145 insertions(+), 128 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 5b97bc90f2..e9fefe033a 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -1,12 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from queue import Queue -from threading import Thread, Event -from time import time from typing import Dict, Optional, Callable -from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo - from UM import i18nCatalog from UM.Logger import Logger from UM.Signal import Signal @@ -15,6 +10,7 @@ from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice from cura.Settings.GlobalStack import GlobalStack +from plugins.UM3NetworkPrinting.src.Network.ZeroConfClient import ZeroConfClient from .ClusterApiClient import ClusterApiClient from .NetworkOutputDevice import NetworkOutputDevice @@ -23,7 +19,6 @@ from .NetworkOutputDevice import NetworkOutputDevice ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. class NetworkOutputDeviceManager: - ZERO_CONF_NAME = u"_ultimaker._tcp.local." MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0") @@ -33,47 +28,24 @@ class NetworkOutputDeviceManager: # Signal emitted when the list of discovered devices changed. discoveredDevicesChanged = Signal() - # Signals emitted when new services were discovered or removed on the network. - addedNetworkCluster = Signal() - removedNetworkCluster = Signal() - def __init__(self) -> None: # Persistent dict containing the networked clusters. self._discovered_devices = {} # type: Dict[str, NetworkOutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() - # TODO: move zeroconf stuff to own class? - self._zero_conf = None # type: Optional[Zeroconf] - self._zero_conf_browser = None # type: Optional[ServiceBrowser] - self._service_changed_request_queue = None # type: Optional[Queue] - self._service_changed_request_event = None # type: Optional[Event] - self._service_changed_request_thread = None # type: Optional[Thread] + # Hook up ZeroConf client. + self._zero_conf_client = ZeroConfClient() + self._zero_conf_client.addedNetworkCluster.connect(self._onAddDevice) + self._zero_conf_client.removedNetworkCluster.connect(self._onRemoveDevice) # TODO: move manual device stuff to own class? # Persistent dict containing manually connected clusters. self._manual_instances = {} # type: Dict[str, Callable] - # Hook up the signals for discovery. - self.addedNetworkCluster.connect(self._onAddDevice) - self.removedNetworkCluster.connect(self._onRemoveDevice) - ## Start the network discovery. - def start(self): - # The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. - # We can also re-schedule the requests when they fail to get detailed service info. - # Any new or re-reschedule requests will be appended to the request queue and the thread will process them. - self._service_changed_request_queue = Queue() - self._service_changed_request_event = Event() - self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True) - self._service_changed_request_thread.start() - - # Start network discovery. - self.stop() - self._zero_conf = Zeroconf() - self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [ - self._appendServiceChangedRequest - ]) + def start(self) -> None: + self._zero_conf_client.start() # Load all manual devices. self._manual_instances = self._getStoredManualInstances() @@ -81,14 +53,8 @@ class NetworkOutputDeviceManager: self.addManualDevice(address) ## Stop network discovery and clean up discovered devices. - def stop(self): - # Cleanup ZeroConf resources. - if self._zero_conf is not None: - self._zero_conf.close() - self._zero_conf = None - if self._zero_conf_browser is not None: - self._zero_conf_browser.cancel() - self._zero_conf_browser = None + def stop(self) -> None: + self._zero_conf_client.stop() # Cleanup all manual devices. for instance_name in list(self._discovered_devices): @@ -254,88 +220,3 @@ class NetworkOutputDeviceManager: @staticmethod def _onApiError(errors) -> None: Logger.log("w", str(errors)) - - ## Appends a service changed request so later the handling thread will pick it up and processes it. - def _appendServiceChangedRequest(self, zeroconf: Zeroconf, service_type, name: str, - state_change: ServiceStateChange) -> None: - item = (zeroconf, service_type, name, state_change) - self._service_changed_request_queue.put(item) - self._service_changed_request_event.set() - - def _handleOnServiceChangedRequests(self) -> None: - while True: - # Wait for the event to be set - self._service_changed_request_event.wait(timeout=5.0) - - # Stop if the application is shutting down - if CuraApplication.getInstance().isShuttingDown(): - return - - self._service_changed_request_event.clear() - - # Handle all pending requests - reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled - while not self._service_changed_request_queue.empty(): - request = self._service_changed_request_queue.get() - zeroconf, service_type, name, state_change = request - try: - result = self._onServiceChanged(zeroconf, service_type, name, state_change) - if not result: - reschedule_requests.append(request) - except Exception: - Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled", - service_type, name) - reschedule_requests.append(request) - - # Re-schedule the failed requests if any - if reschedule_requests: - for request in reschedule_requests: - self._service_changed_request_queue.put(request) - - ## Handler for zeroConf detection. - # Return True or False indicating if the process succeeded. - # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. - def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange - ) -> bool: - if state_change == ServiceStateChange.Added: - return self._onServiceAdded(zero_conf, service_type, name) - elif state_change == ServiceStateChange.Removed: - return self._onServiceRemoved(name) - return True - - ## Handler for when a ZeroConf service was added. - def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: - # First try getting info from zero-conf cache - info = ServiceInfo(service_type, name, properties={}) - for record in zero_conf.cache.entries_with_name(name.lower()): - info.update_record(zero_conf, time(), record) - - for record in zero_conf.cache.entries_with_name(info.server): - info.update_record(zero_conf, time(), record) - if info.address: - break - - # Request more data if info is not complete - if not info.address: - info = zero_conf.get_service_info(service_type, name) - - if info: - type_of_device = info.properties.get(b"type", None) - if type_of_device: - if type_of_device == b"printer": - address = '.'.join(map(lambda n: str(n), info.address)) - self.addedNetworkCluster.emit(str(name), address, info.properties) - else: - Logger.log("w", - "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) - else: - Logger.log("w", "Could not get information about %s" % name) - return False - - return True - - ## Handler for when a ZeroConf service was removed. - def _onServiceRemoved(self, name: str) -> bool: - Logger.log("d", "ZeroConf service removed: %s" % name) - self.removedNetworkCluster.emit(str(name)) - return True diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py new file mode 100644 index 0000000000..4d2828f8c9 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -0,0 +1,136 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from queue import Queue +from threading import Thread, Event +from time import time +from typing import Optional + +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo + +from UM.Logger import Logger +from UM.Signal import Signal +from cura.CuraApplication import CuraApplication + + +## The ZeroConfClient handles all network discovery logic. +# It emits signals when new network services were found or disappeared. +class ZeroConfClient: + + # The discovery protocol name for Ultimaker printers. + ZERO_CONF_NAME = u"_ultimaker._tcp.local." + + # Signals emitted when new services were discovered or removed on the network. + addedNetworkCluster = Signal() + removedNetworkCluster = Signal() + + def __init__(self) -> None: + self._zero_conf = None # type: Optional[Zeroconf] + self._zero_conf_browser = None # type: Optional[ServiceBrowser] + self._service_changed_request_queue = None # type: Optional[Queue] + self._service_changed_request_event = None # type: Optional[Event] + self._service_changed_request_thread = None # type: Optional[Thread] + + ## The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. + # We can also re-schedule the requests when they fail to get detailed service info. + # Any new or re-reschedule requests will be appended to the request queue and the thread will process them. + def start(self) -> None: + self._service_changed_request_queue = Queue() + self._service_changed_request_event = Event() + self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True) + self._service_changed_request_thread.start() + self._zero_conf = Zeroconf() + self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [self._queueService]) + + # Cleanup ZeroConf resources. + def stop(self) -> None: + if self._zero_conf is not None: + self._zero_conf.close() + self._zero_conf = None + if self._zero_conf_browser is not None: + self._zero_conf_browser.cancel() + self._zero_conf_browser = None + + ## Handles a change is discovered network services. + def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None: + item = (zeroconf, service_type, name, state_change) + self._service_changed_request_queue.put(item) + self._service_changed_request_event.set() + + ## Callback for when a ZeroConf service has changes. + def _handleOnServiceChangedRequests(self) -> None: + while True: + # Wait for the event to be set + self._service_changed_request_event.wait(timeout=5.0) + + # Stop if the application is shutting down + if CuraApplication.getInstance().isShuttingDown(): + return + + self._service_changed_request_event.clear() + + # Handle all pending requests + reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled + while not self._service_changed_request_queue.empty(): + request = self._service_changed_request_queue.get() + zeroconf, service_type, name, state_change = request + try: + result = self._onServiceChanged(zeroconf, service_type, name, state_change) + if not result: + reschedule_requests.append(request) + except Exception: + Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled", + service_type, name) + reschedule_requests.append(request) + + # Re-schedule the failed requests if any + if reschedule_requests: + for request in reschedule_requests: + self._service_changed_request_queue.put(request) + + ## Handler for zeroConf detection. + # Return True or False indicating if the process succeeded. + # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. + def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange + ) -> bool: + if state_change == ServiceStateChange.Added: + return self._onServiceAdded(zero_conf, service_type, name) + elif state_change == ServiceStateChange.Removed: + return self._onServiceRemoved(name) + return True + + ## Handler for when a ZeroConf service was added. + def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: + # First try getting info from zero-conf cache + info = ServiceInfo(service_type, name, properties={}) + for record in zero_conf.cache.entries_with_name(name.lower()): + info.update_record(zero_conf, time(), record) + + for record in zero_conf.cache.entries_with_name(info.server): + info.update_record(zero_conf, time(), record) + if info.address: + break + + # Request more data if info is not complete + if not info.address: + info = zero_conf.get_service_info(service_type, name) + + if info: + type_of_device = info.properties.get(b"type", None) + if type_of_device: + if type_of_device == b"printer": + address = '.'.join(map(lambda n: str(n), info.address)) + self.addedNetworkCluster.emit(str(name), address, info.properties) + else: + Logger.log("w", + "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) + else: + Logger.log("w", "Could not get information about %s" % name) + return False + + return True + + ## Handler for when a ZeroConf service was removed. + def _onServiceRemoved(self, name: str) -> bool: + Logger.log("d", "ZeroConf service removed: %s" % name) + self.removedNetworkCluster.emit(str(name)) + return True From 11bd520c987d257a2c8ba505e70f95f968e74866 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 21:49:52 +0200 Subject: [PATCH 17/52] Rename some stuff to make it clear --- .../src/Cloud/CloudOutputDeviceManager.py | 8 +-- .../src/Network/NetworkOutputDeviceManager.py | 52 ++++++++----------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index b6964c7a9c..3ebe8ac90f 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -104,14 +104,14 @@ class CloudOutputDeviceManager: if not device: device = CloudOutputDevice(self._api, cluster_data) discovery.addDiscoveredPrinter(device.key, device.key, cluster_data.friendly_name, - self._createMachineFromDiscoveredPrinter, device.printerType, device) + self._createMachineFromDiscoveredDevice, device.printerType, device) else: discovery.updateDiscoveredPrinter(device.key, cluster_data.friendly_name, device.printerType) new_devices[device.key] = device # Remove output devices that disappeared. - remote_cluster_keys = self._remote_clusters.keys() - removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in remote_cluster_keys] + keys = self._remote_clusters.keys() + removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys] for device in removed_devices: CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) discovery.removeDiscoveredPrinter(device.key) @@ -120,7 +120,7 @@ class CloudOutputDeviceManager: self.discoveredDevicesChanged.emit() self._connectToActiveMachine() - def _createMachineFromDiscoveredPrinter(self, key: str) -> None: + def _createMachineFromDiscoveredDevice(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) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index e9fefe033a..060d58ccdd 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -36,17 +36,15 @@ class NetworkOutputDeviceManager: # Hook up ZeroConf client. self._zero_conf_client = ZeroConfClient() - self._zero_conf_client.addedNetworkCluster.connect(self._onAddDevice) - self._zero_conf_client.removedNetworkCluster.connect(self._onRemoveDevice) + self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered) + self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved) - # TODO: move manual device stuff to own class? # Persistent dict containing manually connected clusters. self._manual_instances = {} # type: Dict[str, Callable] ## Start the network discovery. def start(self) -> None: self._zero_conf_client.start() - # Load all manual devices. self._manual_instances = self._getStoredManualInstances() for address in self._manual_instances: @@ -55,10 +53,9 @@ class NetworkOutputDeviceManager: ## Stop network discovery and clean up discovered devices. def stop(self) -> None: self._zero_conf_client.stop() - # Cleanup all manual devices. for instance_name in list(self._discovered_devices): - self._onRemoveDevice(instance_name) + self._onDiscoveredDeviceRemoved(instance_name) ## Add a networked printer manually by address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: @@ -66,9 +63,9 @@ class NetworkOutputDeviceManager: new_manual_devices = ",".join(self._manual_instances.keys()) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) - key = f"manual:{address}" - if key not in self._discovered_devices: - self._onAddDevice(key, address, { + device_id = f"manual:{address}" + if device_id not in self._discovered_devices: + self._onDeviceDiscovered(device_id, address, { b"name": address.encode("utf-8"), b"address": address.encode("utf-8"), b"manual": b"true", @@ -80,20 +77,18 @@ class NetworkOutputDeviceManager: self._checkManualDevice(address, response_callback) ## Remove a manually added networked printer. - def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: - if key not in self._discovered_devices and address is not None: - key = f"manual:{address}" + def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: + if device_id not in self._discovered_devices and address is not None: + device_id = f"manual:{address}" - if key in self._discovered_devices: - if not address: - address = self._discovered_devices[key].ipAddress - self._onRemoveDevice(key) + if device_id in self._discovered_devices: + address = address or self._discovered_devices[device_id].ipAddress + self._onDiscoveredDeviceRemoved(device_id) if address in self._manual_instances: manual_instance_callback = self._manual_instances.pop(address) - new_manual_devices = ",".join(self._manual_instances.keys()) - CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, - new_manual_devices) + new_devices = ",".join(self._manual_instances.keys()) + CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_devices) if manual_instance_callback: CuraApplication.getInstance().callLater(manual_instance_callback, False, address) @@ -157,18 +152,17 @@ class NetworkOutputDeviceManager: return found_machine_type_identifiers ## Add a new device. - def _onAddDevice(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: + def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: cluster_size = int(properties.get(b"cluster_size", -1)) - printer_type = properties.get(b"machine", b"").decode("utf-8") + machine_identifier = properties.get(b"machine", b"").decode("utf-8") printer_type_identifiers = self._getPrinterTypeIdentifiers() # Detect the machine type based on the BOM number that is sent over the network. + properties[b"printer_type"] = b"Unknown" for bom, p_type in printer_type_identifiers.items(): - if printer_type.startswith(bom): + if machine_identifier.startswith(bom): properties[b"printer_type"] = bytes(p_type, encoding="utf8") break - else: - properties[b"printer_type"] = b"Unknown" # We no longer support legacy devices, so check that here. if cluster_size == -1: @@ -179,7 +173,7 @@ class NetworkOutputDeviceManager: ip_address=address, key=device.getId(), name=device.getName(), - create_callback=self._createMachineFromDiscoveredPrinter, + create_callback=self._createMachineFromDiscoveredDevice, machine_type=device.printerType, device=device ) @@ -188,7 +182,7 @@ class NetworkOutputDeviceManager: self._connectToActiveMachine() ## Remove a device. - def _onRemoveDevice(self, device_id: str) -> None: + def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: device = self._discovered_devices.pop(device_id, None) if not device: return @@ -196,10 +190,10 @@ class NetworkOutputDeviceManager: self.discoveredDevicesChanged.emit() ## Create a machine instance based on the discovered network printer. - def _createMachineFromDiscoveredPrinter(self, key: str) -> None: - device = self._discovered_devices.get(key) + def _createMachineFromDiscoveredDevice(self, device_id: str) -> None: + device = self._discovered_devices.get(device_id) if device is None: - Logger.log("e", "Could not find discovered device with key [%s]", key) + Logger.log("e", "Could not find discovered device with device_id [%s]", device_id) return # The newly added machine is automatically activated. From f34fc7e6f98f31e58a61591b2f08de6c713dd6be Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 21:53:26 +0200 Subject: [PATCH 18/52] Remove tests that are no longer relevant --- .../src/Network/ZeroConfClient.py | 3 +- .../tests/Cloud/Fixtures/__init__.py | 12 -- .../Fixtures/getClusterStatusResponse.json | 95 ----------- .../tests/Cloud/Fixtures/getClusters.json | 17 -- .../Cloud/Fixtures/postJobPrintResponse.json | 8 - .../Cloud/Fixtures/putJobUploadResponse.json | 9 - .../tests/Cloud/Models/__init__.py | 2 - .../tests/Cloud/NetworkManagerMock.py | 105 ------------ .../tests/Cloud/TestCloudApiClient.py | 119 ------------- .../tests/Cloud/TestCloudOutputDevice.py | 157 ------------------ .../Cloud/TestCloudOutputDeviceManager.py | 128 -------------- .../tests/Cloud/__init__.py | 2 - 12 files changed, 1 insertion(+), 656 deletions(-) delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py delete mode 100644 plugins/UM3NetworkPrinting/tests/Cloud/__init__.py diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py index 4d2828f8c9..f70cadb495 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -121,8 +121,7 @@ class ZeroConfClient: address = '.'.join(map(lambda n: str(n), info.address)) self.addedNetworkCluster.emit(str(name), address, info.properties) else: - Logger.log("w", - "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) + Logger.log("w", "The type of the found device is '%s', not 'printer'." % type_of_device) else: Logger.log("w", "Could not get information about %s" % name) return False diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py deleted file mode 100644 index 777afc92c2..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -import json -import os - - -def readFixture(fixture_name: str) -> bytes: - with open("{}/{}.json".format(os.path.dirname(__file__), fixture_name), "rb") as f: - return f.read() - -def parseFixture(fixture_name: str) -> dict: - return json.loads(readFixture(fixture_name).decode()) diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json deleted file mode 100644 index 4f9f47fc75..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "data": { - "generated_time": "2018-12-10T08:23:55.110Z", - "printers": [ - { - "configuration": [ - { - "extruder_index": 0, - "material": { - "material": "empty" - }, - "print_core_id": "AA 0.4" - }, - { - "extruder_index": 1, - "material": { - "material": "empty" - }, - "print_core_id": "AA 0.4" - } - ], - "enabled": true, - "firmware_version": "5.1.2.20180807", - "friendly_name": "Master-Luke", - "ip_address": "10.183.1.140", - "machine_variant": "Ultimaker 3", - "status": "maintenance", - "unique_name": "ultimakersystem-ccbdd30044ec", - "uuid": "b3a47ea3-1eeb-4323-9626-6f9c3c888f9e" - }, - { - "configuration": [ - { - "extruder_index": 0, - "material": { - "brand": "Generic", - "color": "Generic", - "guid": "506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9", - "material": "PLA" - }, - "print_core_id": "AA 0.4" - }, - { - "extruder_index": 1, - "material": { - "brand": "Ultimaker", - "color": "Red", - "guid": "9cfe5bf1-bdc5-4beb-871a-52c70777842d", - "material": "PLA" - }, - "print_core_id": "AA 0.4" - } - ], - "enabled": true, - "firmware_version": "4.3.3.20180529", - "friendly_name": "UM-Marijn", - "ip_address": "10.183.1.166", - "machine_variant": "Ultimaker 3", - "status": "idle", - "unique_name": "ultimakersystem-ccbdd30058ab", - "uuid": "6e62c40a-4601-4b0e-9fec-c7c02c59c30a" - } - ], - "print_jobs": [ - { - "assigned_to": "6e62c40a-4601-4b0e-9fec-c7c02c59c30a", - "configuration": [ - { - "extruder_index": 0, - "material": { - "brand": "Ultimaker", - "color": "Black", - "guid": "3ee70a86-77d8-4b87-8005-e4a1bc57d2ce", - "material": "PLA" - }, - "print_core_id": "AA 0.4" - } - ], - "constraints": {}, - "created_at": "2018-12-10T08:28:04.108Z", - "force": false, - "last_seen": 500165.109491861, - "machine_variant": "Ultimaker 3", - "name": "UM3_dragon", - "network_error_count": 0, - "owner": "Daniel Testing", - "started": false, - "status": "queued", - "time_elapsed": 0, - "time_total": 14145, - "uuid": "d1c8bd52-5e9f-486a-8c25-a123cc8c7702" - } - ] - } -} diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json deleted file mode 100644 index 5200e3b971..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "data": [{ - "cluster_id": "RIZ6cZbWA_Ua7RZVJhrdVfVpf0z-MqaSHQE4v8aRTtYq", - "host_guid": "e90ae0ac-1257-4403-91ee-a44c9b7e8050", - "host_name": "ultimakersystem-ccbdd30044ec", - "host_version": "5.0.0.20170101", - "is_online": true, - "status": "active" - }, { - "cluster_id": "NWKV6vJP_LdYsXgXqAcaNCR0YcLJwar1ugh0ikEZsZs8", - "host_guid": "e0ace90a-91ee-1257-4403-e8050a44c9b7", - "host_name": "ultimakersystem-30044ecccbdd", - "host_version": "5.1.2.20180807", - "is_online": true, - "status": "active" - }] -} diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json deleted file mode 100644 index caedcd8732..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "data": { - "cluster_job_id": "9a59d8e9-91d3-4ff6-b4cb-9db91c4094dd", - "job_id": "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=", - "status": "queued", - "generated_time": "2018-12-10T08:23:55.110Z" - } -} diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json deleted file mode 100644 index 1304f3a9f6..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "data": { - "content_type": "text/plain", - "job_id": "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=", - "job_name": "Ultimaker Robot v3.0", - "status": "uploading", - "upload_url": "https://api.ultimaker.com/print-job-upload" - } -} diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py b/plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py deleted file mode 100644 index f3f6970c54..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py b/plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py deleted file mode 100644 index e504509d67..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -import json -from typing import Dict, Tuple, Union, Optional, Any -from unittest.mock import MagicMock - -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest - -from UM.Logger import Logger -from UM.Signal import Signal - - -class FakeSignal: - def __init__(self): - self._callbacks = [] - - def connect(self, callback): - self._callbacks.append(callback) - - def disconnect(self, callback): - self._callbacks.remove(callback) - - def emit(self, *args, **kwargs): - for callback in self._callbacks: - callback(*args, **kwargs) - - -## This class can be used to mock the QNetworkManager class and test the code using it. -# After patching the QNetworkManager class, requests are prepared before they can be executed. -# Any requests not prepared beforehand will cause KeyErrors. -class NetworkManagerMock: - - # An enumeration of the supported operations and their code for the network access manager. - _OPERATIONS = { - "GET": QNetworkAccessManager.GetOperation, - "POST": QNetworkAccessManager.PostOperation, - "PUT": QNetworkAccessManager.PutOperation, - "DELETE": QNetworkAccessManager.DeleteOperation, - "HEAD": QNetworkAccessManager.HeadOperation, - } # type: Dict[str, int] - - ## Initializes the network manager mock. - def __init__(self) -> None: - # A dict with the prepared replies, using the format {(http_method, url): reply} - self.replies = {} # type: Dict[Tuple[str, str], MagicMock] - self.request_bodies = {} # type: Dict[Tuple[str, str], bytes] - - # Signals used in the network manager. - self.finished = Signal() - self.authenticationRequired = Signal() - - ## Mock implementation of the get, post, put, delete and head methods from the network manager. - # Since the methods are very simple and the same it didn't make sense to repeat the code. - # \param method: The method being called. - # \return The mocked function, if the method name is known. Defaults to the standard getattr function. - def __getattr__(self, method: str) -> Any: - ## This mock implementation will simply return the reply from the prepared ones. - # it raises a KeyError if requests are done without being prepared. - def doRequest(request: QNetworkRequest, body: Optional[bytes] = None, *_): - key = method.upper(), request.url().toString() - if body: - self.request_bodies[key] = body - return self.replies[key] - - operation = self._OPERATIONS.get(method.upper()) - if operation: - return doRequest - - # the attribute is not one of the implemented methods, default to the standard implementation. - return getattr(super(), method) - - ## Prepares a server reply for the given parameters. - # \param method: The HTTP method. - # \param url: The URL being requested. - # \param status_code: The HTTP status code for the response. - # \param response: The response body from the server (generally json-encoded). - def prepareReply(self, method: str, url: str, status_code: int, response: Union[bytes, dict]) -> None: - reply_mock = MagicMock() - reply_mock.url().toString.return_value = url - reply_mock.operation.return_value = self._OPERATIONS[method] - reply_mock.attribute.return_value = status_code - reply_mock.finished = FakeSignal() - reply_mock.isFinished.return_value = False - reply_mock.readAll.return_value = response if isinstance(response, bytes) else json.dumps(response).encode() - self.replies[method, url] = reply_mock - Logger.log("i", "Prepared mock {}-response to {} {}", status_code, method, url) - - ## Gets the request that was sent to the network manager for the given method and URL. - # \param method: The HTTP method. - # \param url: The URL. - def getRequestBody(self, method: str, url: str) -> Optional[bytes]: - return self.request_bodies.get((method.upper(), url)) - - ## Emits the signal that the reply is ready to all prepared replies. - def flushReplies(self) -> None: - for key, reply in self.replies.items(): - Logger.log("i", "Flushing reply to {} {}", *key) - reply.isFinished.return_value = True - reply.finished.emit() - self.finished.emit(reply) - self.reset() - - ## Deletes all prepared replies - def reset(self) -> None: - self.replies.clear() diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py deleted file mode 100644 index 442c422f95..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from typing import List -from unittest import TestCase -from unittest.mock import patch, MagicMock - -from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot - -from ...src.Cloud import CloudApiClient -from ...src.Models.Http.CloudClusterResponse import CloudClusterResponse -from ...src.Models.Http.CloudClusterStatus import CloudClusterStatus -from ...src.Models.Http.CloudPrintJobResponse import CloudPrintJobResponse -from ...src.Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest -from ...src.Models.Http.CloudError import CloudError - -from .Fixtures import readFixture, parseFixture -from .NetworkManagerMock import NetworkManagerMock - - -class TestCloudApiClient(TestCase): - maxDiff = None - - def _errorHandler(self, errors: List[CloudError]): - raise Exception("Received unexpected error: {}".format(errors)) - - def setUp(self): - super().setUp() - self.account = MagicMock() - self.account.isLoggedIn.return_value = True - - self.network = NetworkManagerMock() - with patch.object(CloudApiClient, 'QNetworkAccessManager', return_value = self.network): - self.api = CloudApiClient.CloudApiClient(self.account, self._errorHandler) - - def test_getClusters(self): - result = [] - - response = readFixture("getClusters") - data = parseFixture("getClusters")["data"] - - self.network.prepareReply("GET", CuraCloudAPIRoot + "/connect/v1/clusters", 200, response) - # The callback is a function that adds the result of the call to getClusters to the result list - self.api.getClusters(lambda clusters: result.extend(clusters)) - - self.network.flushReplies() - - self.assertEqual([CloudClusterResponse(**data[0]), CloudClusterResponse(**data[1])], result) - - def test_getClusterStatus(self): - result = [] - - response = readFixture("getClusterStatusResponse") - data = parseFixture("getClusterStatusResponse")["data"] - - url = CuraCloudAPIRoot + "/connect/v1/clusters/R0YcLJwar1ugh0ikEZsZs8NWKV6vJP_LdYsXgXqAcaNC/status" - self.network.prepareReply("GET", url, 200, response) - self.api.getClusterStatus("R0YcLJwar1ugh0ikEZsZs8NWKV6vJP_LdYsXgXqAcaNC", lambda s: result.append(s)) - - self.network.flushReplies() - - self.assertEqual([CloudClusterStatus(**data)], result) - - def test_requestUpload(self): - - results = [] - - response = readFixture("putJobUploadResponse") - - self.network.prepareReply("PUT", CuraCloudAPIRoot + "/cura/v1/jobs/upload", 200, response) - request = CloudPrintJobUploadRequest(job_name = "job name", file_size = 143234, content_type = "text/plain") - self.api.requestUpload(request, lambda r: results.append(r)) - self.network.flushReplies() - - self.assertEqual(["text/plain"], [r.content_type for r in results]) - self.assertEqual(["uploading"], [r.status for r in results]) - - def test_uploadToolPath(self): - - results = [] - progress = MagicMock() - - data = parseFixture("putJobUploadResponse")["data"] - upload_response = CloudPrintJobResponse(**data) - - # Network client doesn't look into the reply - self.network.prepareReply("PUT", upload_response.upload_url, 200, b'{}') - - mesh = ("1234" * 100000).encode() - self.api.uploadToolPath(upload_response, mesh, lambda: results.append("sent"), progress.advance, progress.error) - - for _ in range(10): - self.network.flushReplies() - self.network.prepareReply("PUT", upload_response.upload_url, 200, b'{}') - - self.assertEqual(["sent"], results) - - def test_requestPrint(self): - - results = [] - - response = readFixture("postJobPrintResponse") - - cluster_id = "NWKV6vJP_LdYsXgXqAcaNCR0YcLJwar1ugh0ikEZsZs8" - cluster_job_id = "9a59d8e9-91d3-4ff6-b4cb-9db91c4094dd" - job_id = "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=" - - self.network.prepareReply("POST", - CuraCloudAPIRoot + "/connect/v1/clusters/{}/print/{}" - .format(cluster_id, job_id), - 200, response) - - self.api.requestPrint(cluster_id, job_id, lambda r: results.append(r)) - - self.network.flushReplies() - - self.assertEqual([job_id], [r.job_id for r in results]) - self.assertEqual([cluster_job_id], [r.cluster_job_id for r in results]) - self.assertEqual(["queued"], [r.status for r in results]) diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py deleted file mode 100644 index 07c44753c4..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -import json -from unittest import TestCase -from unittest.mock import patch, MagicMock - -from UM.Scene.SceneNode import SceneNode -from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot -from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel -from ...src.Cloud import CloudApiClient -from ...src.Cloud.CloudOutputDevice import CloudOutputDevice -from ...src.Models.Http.CloudClusterResponse import CloudClusterResponse -from .Fixtures import readFixture, parseFixture -from .NetworkManagerMock import NetworkManagerMock - - -class TestCloudOutputDevice(TestCase): - maxDiff = None - - CLUSTER_ID = "RIZ6cZbWA_Ua7RZVJhrdVfVpf0z-MqaSHQE4v8aRTtYq" - JOB_ID = "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=" - 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) - REQUEST_UPLOAD_URL = "{}/cura/v1/jobs/upload".format(CuraCloudAPIRoot) - - def setUp(self): - super().setUp() - self.app = MagicMock() - - self.patches = [patch("UM.Qt.QtApplication.QtApplication.getInstance", return_value=self.app), - patch("UM.Application.Application.getInstance", return_value=self.app)] - for patched_method in self.patches: - 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, - friendly_name=self.FRIENDLY_NAME) - - self.network = NetworkManagerMock() - self.account = MagicMock(isLoggedIn=True, accessToken="TestAccessToken") - self.onError = MagicMock() - with patch.object(CloudApiClient, "QNetworkAccessManager", return_value = self.network): - self._api = CloudApiClient.CloudApiClient(self.account, self.onError) - - self.device = CloudOutputDevice(self._api, self.cluster) - self.cluster_status = parseFixture("getClusterStatusResponse") - self.network.prepareReply("GET", self.STATUS_URL, 200, readFixture("getClusterStatusResponse")) - - def tearDown(self): - try: - super().tearDown() - self.network.flushReplies() - finally: - for patched_method in self.patches: - patched_method.stop() - - # 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.FRIENDLY_NAME) - - def test_status(self): - self.device._update() - self.network.flushReplies() - - self.assertEqual([PrinterOutputModel, PrinterOutputModel], [type(printer) for printer in self.device.printers]) - - controller_fields = { - "_output_device": self.device, - "can_abort": True, - "can_control_manually": False, - "can_pause": True, - "can_pre_heat_bed": False, - "can_pre_heat_hotends": False, - "can_send_raw_gcode": False, - "can_update_firmware": False, - } - - self.assertEqual({printer["uuid"] for printer in self.cluster_status["data"]["printers"]}, - {printer.key for printer in self.device.printers}) - self.assertEqual([controller_fields, controller_fields], - [printer.getController().__dict__ for printer in self.device.printers]) - - self.assertEqual(["UM3PrintJobOutputModel"], [type(printer).__name__ for printer in self.device.printJobs]) - self.assertEqual({job["uuid"] for job in self.cluster_status["data"]["print_jobs"]}, - {job.key for job in self.device.printJobs}) - self.assertEqual({job["owner"] for job in self.cluster_status["data"]["print_jobs"]}, - {job.owner for job in self.device.printJobs}) - self.assertEqual({job["name"] for job in self.cluster_status["data"]["print_jobs"]}, - {job.name for job in self.device.printJobs}) - - def test_remove_print_job(self): - self.device._update() - self.network.flushReplies() - self.assertEqual(1, len(self.device.printJobs)) - - self.cluster_status["data"]["print_jobs"].clear() - self.network.prepareReply("GET", self.STATUS_URL, 200, self.cluster_status) - - self.device._last_request_time = None - self.device._update() - self.network.flushReplies() - self.assertEqual([], self.device.printJobs) - - def test_remove_printers(self): - self.device._update() - self.network.flushReplies() - self.assertEqual(2, len(self.device.printers)) - - self.cluster_status["data"]["printers"].clear() - self.network.prepareReply("GET", self.STATUS_URL, 200, self.cluster_status) - - self.device._last_request_time = None - self.device._update() - self.network.flushReplies() - self.assertEqual([], self.device.printers) - - def test_print_to_cloud(self): - active_machine_mock = self.app.getGlobalContainerStack.return_value - active_machine_mock.getMetaDataEntry.side_effect = {"file_formats": "application/x-ufp"}.get - - request_upload_response = parseFixture("putJobUploadResponse") - request_print_response = parseFixture("postJobPrintResponse") - self.network.prepareReply("PUT", self.REQUEST_UPLOAD_URL, 201, request_upload_response) - self.network.prepareReply("PUT", request_upload_response["data"]["upload_url"], 201, b"{}") - self.network.prepareReply("POST", self.PRINT_URL, 200, request_print_response) - - file_handler = MagicMock() - file_handler.getSupportedFileTypesWrite.return_value = [{ - "extension": "ufp", - "mime_type": "application/x-ufp", - "mode": 2 - }, { - "extension": "gcode.gz", - "mime_type": "application/gzip", - "mode": 2, - }] - file_handler.getWriterByMimeType.return_value.write.side_effect = \ - lambda stream, nodes: stream.write(str(nodes).encode()) - - scene_nodes = [SceneNode()] - expected_mesh = str(scene_nodes).encode() - self.device.requestWrite(scene_nodes, file_handler=file_handler, file_name="FileName") - - self.network.flushReplies() - self.assertEqual( - {"data": {"content_type": "application/x-ufp", "file_size": len(expected_mesh), "job_name": "FileName"}}, - json.loads(self.network.getRequestBody("PUT", self.REQUEST_UPLOAD_URL).decode()) - ) - self.assertEqual(expected_mesh, - self.network.getRequestBody("PUT", request_upload_response["data"]["upload_url"])) - self.assertIsNone(self.network.getRequestBody("POST", self.PRINT_URL)) diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py deleted file mode 100644 index 105f42c798..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from unittest import TestCase -from unittest.mock import patch, MagicMock - -from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager -from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot - -from ...src.Cloud import CloudApiClient -from ...src.Cloud import CloudOutputDeviceManager -from ...src.Models.Http.CloudClusterResponse import CloudClusterResponse -from .Fixtures import parseFixture, readFixture - -from .NetworkManagerMock import NetworkManagerMock, FakeSignal - - -class TestCloudOutputDeviceManager(TestCase): - maxDiff = None - - URL = CuraCloudAPIRoot + "/connect/v1/clusters" - - def setUp(self): - super().setUp() - self.app = MagicMock() - self.device_manager = OutputDeviceManager() - self.app.getOutputDeviceManager.return_value = self.device_manager - - self.patches = [patch("UM.Qt.QtApplication.QtApplication.getInstance", return_value=self.app), - patch("UM.Application.Application.getInstance", return_value=self.app)] - for patched_method in self.patches: - patched_method.start() - - self.network = NetworkManagerMock() - self.timer = MagicMock(timeout = FakeSignal()) - with patch.object(CloudApiClient, "QNetworkAccessManager", return_value = self.network), \ - patch.object(CloudOutputDeviceManager, "QTimer", return_value = self.timer): - self.manager = CloudOutputDeviceManager.CloudOutputDeviceManager() - self.clusters_response = parseFixture("getClusters") - self.network.prepareReply("GET", self.URL, 200, readFixture("getClusters")) - - def tearDown(self): - try: - self._beforeTearDown() - - self.network.flushReplies() - self.manager.stop() - for patched_method in self.patches: - patched_method.stop() - finally: - super().tearDown() - - ## Before tear down method we check whether the state of the output device manager is what we expect based on the - # mocked API response. - def _beforeTearDown(self): - # let the network send replies - self.network.flushReplies() - # get the created devices - devices = self.device_manager.getOutputDevices() - # TODO: Check active device - - response_clusters = [] - for cluster in self.clusters_response.get("data", []): - response_clusters.append(CloudClusterResponse(**cluster).toDict()) - manager_clusters = sorted([device.clusterData.toDict() for device in self.manager._remote_clusters.values()], - key=lambda cluster: cluster['cluster_id'], reverse=True) - self.assertEqual(response_clusters, manager_clusters) - - ## Runs the initial request to retrieve the clusters. - def _loadData(self): - self.manager.start() - self.network.flushReplies() - - def test_device_is_created(self): - # just create the cluster, it is checked at tearDown - self._loadData() - - def test_device_is_updated(self): - self._loadData() - - # update the cluster from member variable, which is checked at tearDown - self.clusters_response["data"][0]["host_name"] = "New host name" - self.network.prepareReply("GET", self.URL, 200, self.clusters_response) - - self.manager._update_timer.timeout.emit() - - def test_device_is_removed(self): - self._loadData() - - # delete the cluster from member variable, which is checked at tearDown - del self.clusters_response["data"][1] - self.network.prepareReply("GET", self.URL, 200, self.clusters_response) - - self.manager._update_timer.timeout.emit() - - def test_device_connects_by_cluster_id(self): - active_machine_mock = self.app.getGlobalContainerStack.return_value - cluster1, cluster2 = self.clusters_response["data"] - cluster_id = cluster1["cluster_id"] - active_machine_mock.getMetaDataEntry.side_effect = {"um_cloud_cluster_id": cluster_id}.get - - self._loadData() - - self.assertTrue(self.device_manager.getOutputDevice(cluster1["cluster_id"]).isConnected()) - self.assertIsNone(self.device_manager.getOutputDevice(cluster2["cluster_id"])) - self.assertEqual([], active_machine_mock.setMetaDataEntry.mock_calls) - - def test_device_connects_by_network_key(self): - active_machine_mock = self.app.getGlobalContainerStack.return_value - - cluster1, cluster2 = self.clusters_response["data"] - network_key = cluster2["host_name"] + ".ultimaker.local" - active_machine_mock.getMetaDataEntry.side_effect = {"um_network_key": network_key}.get - - self._loadData() - - self.assertIsNone(self.device_manager.getOutputDevice(cluster1["cluster_id"])) - self.assertTrue(self.device_manager.getOutputDevice(cluster2["cluster_id"]).isConnected()) - - active_machine_mock.setMetaDataEntry.assert_called_with("um_cloud_cluster_id", cluster2["cluster_id"]) - - @patch.object(CloudOutputDeviceManager, "Message") - def test_api_error(self, message_mock): - self.clusters_response = { - "errors": [{"id": "notFound", "title": "Not found!", "http_status": "404", "code": "notFound"}] - } - self.network.prepareReply("GET", self.URL, 200, self.clusters_response) - self._loadData() - message_mock.return_value.show.assert_called_once_with() diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/__init__.py b/plugins/UM3NetworkPrinting/tests/Cloud/__init__.py deleted file mode 100644 index f3f6970c54..0000000000 --- a/plugins/UM3NetworkPrinting/tests/Cloud/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. From fa5b083b742e3f18b965dce4530418baa3b0b55a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 21:58:29 +0200 Subject: [PATCH 19/52] Small code improvements --- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 3 +-- plugins/UM3NetworkPrinting/src/Models/BaseModel.py | 3 ++- plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py | 3 ++- .../src/Models/Http/ClusterPrinterConfigurationMaterial.py | 2 ++ plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py | 3 ++- .../UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py | 2 +- .../src/Network/NetworkOutputDeviceManager.py | 2 +- .../src/UltimakerNetworkedPrinterOutputDevice.py | 3 ++- plugins/UM3NetworkPrinting/src/Utils.py | 2 ++ 9 files changed, 15 insertions(+), 8 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 0147851343..6e8ce73bf5 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from time import time -from typing import List, Optional, Set, cast +from typing import List, Optional, cast from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtGui import QDesktopServices @@ -13,7 +13,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.Scene.SceneNode import SceneNode from UM.Version import Version - from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index 131b71ab43..3fb83c330f 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -1,4 +1,5 @@ -## Base model that maps kwargs to instance attributes. +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime, timezone from typing import TypeVar, Dict, List, Any, Type, Union diff --git a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py index 8687b015ce..31f5a39fac 100644 --- a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py @@ -1,4 +1,5 @@ -## Class representing a material that was fetched from the cluster API. +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from .BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index 1d0ef2b708..57e763e159 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -1,3 +1,5 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from typing import Optional from UM.Logger import Logger diff --git a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py index 9e606fd1c4..f3a67686cb 100644 --- a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py @@ -1,4 +1,5 @@ -## Class representing a local material that was fetched from the container registry. +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from .BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py index 3462fc4a54..1a27fc3f73 100644 --- a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py @@ -15,7 +15,7 @@ class UM3PrintJobOutputModel(PrintJobOutputModel): def __init__(self, output_controller: PrinterOutputController, key: str = "", name: str = "", parent=None) -> None: super().__init__(output_controller, key, name, parent) - self._configuration_changes = [] # type: List[ConfigurationChangeModel] + self._configuration_changes = [] # type: List[ConfigurationChangeModel] @pyqtProperty("QVariantList", notify=configurationChangesChanged) def configurationChanges(self) -> List[ConfigurationChangeModel]: diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 060d58ccdd..6bed02549f 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -10,8 +10,8 @@ from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice from cura.Settings.GlobalStack import GlobalStack -from plugins.UM3NetworkPrinting.src.Network.ZeroConfClient import ZeroConfClient +from .ZeroConfClient import ZeroConfClient from .ClusterApiClient import ClusterApiClient from .NetworkOutputDevice import NetworkOutputDevice diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index b098be629e..11977a9d10 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -1,6 +1,7 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os +from abc import ABC from typing import List, Optional, Dict from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot, QUrl @@ -22,7 +23,7 @@ from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus ## Output device class that forms the basis of Ultimaker networked printer output devices. # Currently used for local networking and cloud printing using Ultimaker Connect. # This base class primarily contains all the Qt properties and slots needed for the monitor page to work. -class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): +class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice, ABC): # Signal emitted when the status of the print jobs for this cluster were changed over the network. printJobsChanged = pyqtSignal() diff --git a/plugins/UM3NetworkPrinting/src/Utils.py b/plugins/UM3NetworkPrinting/src/Utils.py index 410dd27d1d..8872576f8b 100644 --- a/plugins/UM3NetworkPrinting/src/Utils.py +++ b/plugins/UM3NetworkPrinting/src/Utils.py @@ -1,3 +1,5 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime, timedelta from UM import i18nCatalog From 529b483f36187d1681463c2993558ffbe7a60ea2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 22:00:48 +0200 Subject: [PATCH 20/52] Remove ABC --- .../src/UltimakerNetworkedPrinterOutputDevice.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 11977a9d10..b098be629e 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -1,7 +1,6 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os -from abc import ABC from typing import List, Optional, Dict from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot, QUrl @@ -23,7 +22,7 @@ from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus ## Output device class that forms the basis of Ultimaker networked printer output devices. # Currently used for local networking and cloud printing using Ultimaker Connect. # This base class primarily contains all the Qt properties and slots needed for the monitor page to work. -class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice, ABC): +class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Signal emitted when the status of the print jobs for this cluster were changed over the network. printJobsChanged = pyqtSignal() From b90e5b3262f299f98ed447d58e618e55540bcd5a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 23:12:55 +0200 Subject: [PATCH 21/52] re-implement requestWrite --- .../src/Cloud/CloudOutputDevice.py | 17 +- .../src/Network/NetworkOutputDevice.py | 329 +++++------------- ...ge.py => PrintJobUploadProgressMessage.py} | 4 +- .../UltimakerNetworkedPrinterOutputDevice.py | 4 + 4 files changed, 92 insertions(+), 262 deletions(-) rename plugins/UM3NetworkPrinting/src/{Cloud/CloudProgressMessage.py => PrintJobUploadProgressMessage.py} (93%) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 6e8ce73bf5..f3e51c5f4e 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -17,7 +17,6 @@ from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from .CloudProgressMessage import CloudProgressMessage from .CloudApiClient import CloudApiClient from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice from ..MeshFormatHandler import MeshFormatHandler @@ -84,15 +83,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._account = api_client.account self._cluster = cluster self.setAuthenticationState(AuthState.NotAuthenticated) - self._setInterfaceElements() # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) - # We only allow a single upload at a time. - self._progress = CloudProgressMessage() - # Keep server string of the last generated time to avoid updating models more than once for the same response self._received_printers = None # type: Optional[List[ClusterPrinterStatus]] self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]] @@ -143,15 +138,14 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) ## Called when Cura requests an output device to receive a (G-code) file. - def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: # Show an error message if we're already sending a job. if self._progress.visible: message = Message( - text=I18N_CATALOG.i18nc("@info:status", - "Sending new jobs (temporarily) blocked, still sending the previous print job."), - title=I18N_CATALOG.i18nc("@info:title", "Cloud error"), + text=I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), + title=I18N_CATALOG.i18nc("@info:title", "Print error"), lifetime=10 ) message.show() @@ -170,8 +164,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): Logger.log("e", "Missing file or mesh writer!") return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job.")) + # TODO: use stream just like the network output device mesh = mesh_format.getBytes(nodes) - self._tool_path = mesh request = CloudPrintJobUploadRequest( job_name=file_name or mesh_format.file_extension, @@ -236,7 +230,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Shows a message when the upload has succeeded # \param response: The response from the cloud API. def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None: - Logger.log("d", "The cluster will be printing this print job with the ID %s", response.cluster_job_id) self._progress.hide() Message( text=I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 0f516a8aeb..25e7b467f8 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -6,13 +6,17 @@ from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty from UM.FileHandler.FileHandler import FileHandler +from UM.FileHandler.WriteFileJob import WriteFileJob from UM.Logger import Logger +from UM.Message import Message from UM.i18n import i18nCatalog from UM.Scene.SceneNode import SceneNode +from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from .ClusterApiClient import ClusterApiClient +from ..MeshFormatHandler import MeshFormatHandler from ..SendMaterialJob import SendMaterialJob from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice @@ -38,22 +42,9 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._cluster_api = ClusterApiClient(address, on_error=self._onNetworkError) # We don't have authentication over local networking, so we're always authenticated. self.setAuthenticationState(AuthState.Authenticated) - self._setInterfaceElements() - self._active_camera_url = QUrl() # type: QUrl - # self._number_of_extruders = 2 - # self._dummy_lambdas = ( - # "", {}, io.BytesIO() - # ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]] - # self._error_message = None # type: Optional[Message] - # self._write_job_progress_message = None # type: Optional[Message] - # self._progress_message = None # type: Optional[Message] - # self._printer_selection_dialog = None # type: QObject - # self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] - # self._finished_jobs = [] # type: List[UM3PrintJobOutputModel] - # self._sending_job = None ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: @@ -117,195 +108,13 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # if print_job.getPreviewImage() is None: # self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) - ## Sync the material profiles in Cura with the printer. - # This gets called when connecting to a printer as well as when sending a print. + ## Sync the material profiles in Cura with the printer. + # This gets called when connecting to a printer as well as when sending a print. def sendMaterialProfiles(self) -> None: job = SendMaterialJob(device=self) job.run() - - - # TODO FROM HERE - - - - def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, - file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: - pass - # self.writeStarted.emit(self) - # - # self.sendMaterialProfiles() - # - # mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) - # - # # This function pauses with the yield, waiting on instructions on which printer it needs to print with. - # if not mesh_format.is_valid: - # Logger.log("e", "Missing file or mesh writer!") - # return - # self._sending_job = self._sendPrintJob(mesh_format, nodes) - # if self._sending_job is not None: - # self._sending_job.send(None) # Start the generator. - # - # if len(self._printers) > 1: # We need to ask the user. - # self._spawnPrinterSelectionDialog() - # else: # Just immediately continue. - # self._sending_job.send("") # No specifically selected printer. - # self._sending_job.send(None) - # - # def _spawnPrinterSelectionDialog(self): - # if self._printer_selection_dialog is None: - # if PluginRegistry.getInstance() is not None: - # path = os.path.join( - # PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - # "resources", "qml", "PrintWindow.qml" - # ) - # self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self}) - # if self._printer_selection_dialog is not None: - # self._printer_selection_dialog.show() - - # ## Allows the user to choose a printer to print with from the printer - # # selection dialogue. - # # \param target_printer The name of the printer to target. - # @pyqtSlot(str) - # def selectPrinter(self, target_printer: str = "") -> None: - # if self._sending_job is not None: - # self._sending_job.send(target_printer) - - # @pyqtSlot() - # def cancelPrintSelection(self) -> None: - # self._sending_gcode = False - - # ## Greenlet to send a job to the printer over the network. - # # - # # This greenlet gets called asynchronously in requestWrite. It is a - # # greenlet in order to optionally wait for selectPrinter() to select a - # # printer. - # # The greenlet yields exactly three times: First time None, - # # \param mesh_format Object responsible for choosing the right kind of format to write with. - # def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]): - # Logger.log("i", "Sending print job to printer.") - # if self._sending_gcode: - # self._error_message = Message( - # I18N_CATALOG.i18nc("@info:status", - # "Sending new jobs (temporarily) blocked, still sending the previous print job.")) - # self._error_message.show() - # yield #Wait on the user to select a target printer. - # yield #Wait for the write job to be finished. - # yield False #Return whether this was a success or not. - # yield #Prevent StopIteration. - # - # self._sending_gcode = True - # - # # Potentially wait on the user to select a target printer. - # target_printer = yield # type: Optional[str] - # - # # Using buffering greatly reduces the write time for many lines of gcode - # - # stream = mesh_format.createStream() - # - # job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode) - # - # self._write_job_progress_message = Message(I18N_CATALOG.i18nc("@info:status", "Sending data to printer"), - # lifetime = 0, dismissable = False, progress = -1, - # title = I18N_CATALOG.i18nc("@info:title", "Sending Data"), - # use_inactivity_timer = False) - # self._write_job_progress_message.show() - # - # if mesh_format.preferred_format is not None: - # self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream) - # job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) - # job.start() - # yield True # Return that we had success! - # yield # To prevent having to catch the StopIteration exception. - - # def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: - # if self._write_job_progress_message: - # self._write_job_progress_message.hide() - # - # self._progress_message = Message(I18N_CATALOG.i18nc("@info:status", "Sending data to printer"), lifetime = 0, - # dismissable = False, progress = -1, - # title = I18N_CATALOG.i18nc("@info:title", "Sending Data")) - # self._progress_message.addAction("Abort", I18N_CATALOG.i18nc("@action:button", "Cancel"), icon = "", - # description = "") - # self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) - # self._progress_message.show() - # parts = [] - # - # target_printer, preferred_format, stream = self._dummy_lambdas - # - # # If a specific printer was selected, it should be printed with that machine. - # if target_printer: - # target_printer = self._printer_uuid_to_unique_name_mapping[target_printer] - # parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) - # - # # Add user name to the print_job - # parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) - # - # file_name = self._application.getPrintInformation().jobName + "." + preferred_format["extension"] - # - # output = stream.getvalue() # Either str or bytes depending on the output mode. - # if isinstance(stream, io.StringIO): - # output = cast(str, output).encode("utf-8") - # output = cast(bytes, output) - # - # parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) - # - # self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, - # on_finished = self._onPostPrintJobFinished, - # on_progress = self._onUploadPrintJobProgress) - - # def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: - # if self._progress_message: - # self._progress_message.hide() - # self._compressing_gcode = False - # self._sending_gcode = False - - # def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: - # if bytes_total > 0: - # new_progress = bytes_sent / bytes_total * 100 - # # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get - # # timeout responses if this happens. - # self._last_response_time = time() - # if self._progress_message is not None and new_progress != self._progress_message.getProgress(): - # self._progress_message.show() # Ensure that the message is visible. - # self._progress_message.setProgress(bytes_sent / bytes_total * 100) - # - # # If successfully sent: - # if bytes_sent == bytes_total: - # # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to - # # the monitor tab. - # self._success_message = Message( - # I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), - # lifetime=5, dismissable=True, - # title=I18N_CATALOG.i18nc("@info:title", "Data Sent")) - # self._success_message.addAction("View", I18N_CATALOG.i18nc("@action:button", "View in Monitor"), icon = "", - # description="") - # self._success_message.actionTriggered.connect(self._successMessageActionTriggered) - # self._success_message.show() - # else: - # if self._progress_message is not None: - # self._progress_message.setProgress(0) - # self._progress_message.hide() - - # def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: - # if action_id == "Abort": - # Logger.log("d", "User aborted sending print to remote.") - # if self._progress_message is not None: - # self._progress_message.hide() - # self._compressing_gcode = False - # self._sending_gcode = False - # self._application.getController().setActiveStage("PrepareStage") - # - # # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request - # # the "reply" should be disconnected - # if self._latest_reply_handler: - # self._latest_reply_handler.disconnect() - # self._latest_reply_handler = None - - # def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: - # if action_id == "View": - # self._application.getController().setActiveStage("MonitorStage") - + # ## Callback for when preview image was downloaded from cluster. # def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: # reply_url = reply.url().toString() # @@ -317,53 +126,77 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # image.loadFromData(reply.readAll()) # print_job.updatePreviewImage(image) - # def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel": - # material_manager = self._application.getMaterialManager() - # material_group_list = None - # - # # Avoid crashing if there is no "guid" field in the metadata - # material_guid = material_data.get("guid") - # if material_guid: - # material_group_list = material_manager.getMaterialGroupListByGUID(material_guid) - # - # # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the - # # material is unknown to Cura, so we should return an "empty" or "unknown" material model. - # if material_group_list is None: - # material_name = I18N_CATALOG.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \ - # else I18N_CATALOG.i18nc("@label:material", "Unknown") - # - # return MaterialOutputModel(guid = material_data.get("guid", ""), - # type = material_data.get("material", ""), - # color = material_data.get("color", ""), - # brand = material_data.get("brand", ""), - # name = material_data.get("name", material_name) - # ) - # - # # Sort the material groups by "is_read_only = True" first, and then the name alphabetically. - # read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list)) - # non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list)) - # material_group = None - # if read_only_material_group_list: - # read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name) - # material_group = read_only_material_group_list[0] - # elif non_read_only_material_group_list: - # non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name) - # material_group = non_read_only_material_group_list[0] - # - # if material_group: - # container = material_group.root_material_node.getContainer() - # color = container.getMetaDataEntry("color_code") - # brand = container.getMetaDataEntry("brand") - # material_type = container.getMetaDataEntry("material") - # name = container.getName() - # else: - # Logger.log("w", - # "Unable to find material with guid {guid}. Using data as provided by cluster".format( - # guid=material_data["guid"])) - # color = material_data["color"] - # brand = material_data["brand"] - # material_type = material_data["material"] - # name = I18N_CATALOG.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \ - # else I18N_CATALOG.i18nc("@label:material", "Unknown") - # return MaterialOutputModel(guid = material_data["guid"], type = material_type, - # brand = brand, color = color, name = name) + ## Send a print job to the cluster. + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, + file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: + + # Show an error message if we're already sending a job. + if self._progress.visible: + message = Message( + text=I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), + title=I18N_CATALOG.i18nc("@info:title", "Print error"), + lifetime=10 + ) + message.show() + return + + self.writeStarted.emit(self) + + # Make sure the printer is aware of all new materials as the new print job might contain one. + self.sendMaterialProfiles() + + # Detect the correct export format depending on printer type and firmware version. + mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) + if not mesh_format.is_valid: + Logger.log("e", "Missing file or mesh writer!") + return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job.")) + + # Determine the filename. + job_name = CuraApplication.getInstance().getPrintInformation().jobName + extension = mesh_format.preferred_format.get("extension", "") + file_name = f"{job_name}.{extension}" + + # Export the file. + stream = mesh_format.createStream() + job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode) + job.setFileName(file_name) + job.finished.connect(self._onPrintJobCreated) + job.start() + + ## Handler for when the print job was created locally. + # It can now be sent over the network. + def _onPrintJobCreated(self, job: WriteFileJob) -> None: + self._progress.show() + parts = [] + parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) + output = job.getStream().getvalue() + parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), output)) + self.postFormWithParts("print_jobs/", parts, on_finished=self._onPrintUploadCompleted, + on_progress=self._onPrintJobUploadProgress) + + ## Handler for print job upload progress. + def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None: + percentage = (bytes_sent / bytes_total) if bytes_total else 0 + self._progress.setProgress(percentage * 100) + self.writeProgress.emit() + + ## Handler for when the print job was fully uploaded to the cluster. + def _onPrintUploadCompleted(self) -> None: + self._progress.hide() + Message( + text=I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), + title=I18N_CATALOG.i18nc("@info:title", "Data Sent"), + lifetime=5 + ).show() + self.writeFinished.emit() + + ## Displays the given message if uploading the mesh has failed + # \param message: The message to display. + def _onUploadError(self, message: str = None) -> None: + self._progress.hide() + Message( + text=message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."), + title=I18N_CATALOG.i18nc("@info:title", "Network error"), + lifetime=10 + ).show() + self.writeError.emit() diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py b/plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py similarity index 93% rename from plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py rename to plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py index 943bef2bc1..9862c9ec72 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py +++ b/plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py @@ -8,11 +8,11 @@ I18N_CATALOG = i18nCatalog("cura") ## Class responsible for showing a progress message while a mesh is being uploaded to the cloud. -class CloudProgressMessage(Message): +class PrintJobUploadProgressMessage(Message): def __init__(self): super().__init__( title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"), - text = I18N_CATALOG.i18nc("@info:status", "Uploading via Ultimaker Cloud"), + text = I18N_CATALOG.i18nc("@info:status", "Uploading print job to printer."), progress = -1, lifetime = 0, dismissable = False, diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index b098be629e..ee6a67992f 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -14,6 +14,7 @@ from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from .Utils import formatTimeCompleted, formatDateCompleted from .ClusterOutputController import ClusterOutputController +from .PrintJobUploadProgressMessage import PrintJobUploadProgressMessage from .Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus @@ -60,6 +61,9 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Load the Monitor UI elements. self._loadMonitorTab() + # The job upload progress message modal. + self._progress = PrintJobUploadProgressMessage() + ## The IP address of the printer. @pyqtProperty(str, constant=True) def address(self) -> str: From 8087fa421ddb97823ba6adb4fd3dbc8c631636f1 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 23:25:08 +0200 Subject: [PATCH 22/52] Make params named so we know what we're doing --- plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 25e7b467f8..5e66bdacf8 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -158,7 +158,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Export the file. stream = mesh_format.createStream() - job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode) + job = WriteFileJob(writer=mesh_format.writer, stream=stream, data=nodes, mode=mesh_format.file_mode) job.setFileName(file_name) job.finished.connect(self._onPrintJobCreated) job.start() From 9e4c71cce3ee300327f01badd70246b673528bfc Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 29 Jul 2019 23:51:20 +0200 Subject: [PATCH 23/52] Fixes for print job upload --- plugins/UM3NetworkPrinting/src/MeshFormatHandler.py | 8 +++++--- .../src/Network/NetworkOutputDevice.py | 9 +++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index c3cd82a86d..2f01743dd6 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -90,9 +90,11 @@ class MeshFormatHandler: machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";") machine_file_formats = [file_type.strip() for file_type in machine_file_formats] - # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. - if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"): - machine_file_formats = ["application/x-ufp"] + machine_file_formats + + # TODO: re-enable UFP after Cura master branch works again + # # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. + # if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"): + # machine_file_formats = ["application/x-ufp"] + machine_file_formats # Take the intersection between file_formats and machine_file_formats. format_by_mimetype = {f["mime_type"]: f for f in file_formats} diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 5e66bdacf8..7249683f4b 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -4,6 +4,7 @@ from typing import Optional, Dict, List, Any from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty +from PyQt5.QtNetwork import QNetworkReply from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.WriteFileJob import WriteFileJob @@ -167,11 +168,15 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # It can now be sent over the network. def _onPrintJobCreated(self, job: WriteFileJob) -> None: self._progress.show() + # TODO: extract multi-part stuff parts = [] parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) output = job.getStream().getvalue() + if isinstance(output, str): + # Ensure that our output is bytes + output = output.encode("utf-8") parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), output)) - self.postFormWithParts("print_jobs/", parts, on_finished=self._onPrintUploadCompleted, + self.postFormWithParts("/cluster-api/v1/print_jobs/", parts, on_finished=self._onPrintUploadCompleted, on_progress=self._onPrintJobUploadProgress) ## Handler for print job upload progress. @@ -181,7 +186,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.writeProgress.emit() ## Handler for when the print job was fully uploaded to the cluster. - def _onPrintUploadCompleted(self) -> None: + def _onPrintUploadCompleted(self, reply: QNetworkReply) -> None: self._progress.hide() Message( text=I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), From bfca117bff04165852c96a8ea89824d1a0b198b6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 13:09:29 +0200 Subject: [PATCH 24/52] Fixes --- .../src/Cloud/CloudOutputDevice.py | 88 ++++++++++--------- .../src/Cloud/CloudOutputDeviceManager.py | 47 +++++----- .../UM3NetworkPrinting/src/ExportFileJob.py | 39 ++++++++ .../src/Network/NetworkOutputDevice.py | 31 ++----- .../src/Network/NetworkOutputDeviceManager.py | 4 +- .../src/UM3OutputDevicePlugin.py | 20 ----- 6 files changed, 119 insertions(+), 110 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/ExportFileJob.py diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index f3e51c5f4e..446a8bb73b 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -9,6 +9,7 @@ from PyQt5.QtGui import QDesktopServices from UM import i18nCatalog from UM.Backend.Backend import BackendState from UM.FileHandler.FileHandler import FileHandler +from UM.FileHandler.WriteFileJob import WriteFileJob from UM.Logger import Logger from UM.Message import Message from UM.Scene.SceneNode import SceneNode @@ -16,6 +17,7 @@ from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob from .CloudApiClient import CloudApiClient from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice @@ -93,6 +95,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]] # Reference to the uploaded print job / mesh + # We do this to prevent re-uploading the same file multiple times. self._tool_path = None # type: Optional[bytes] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] @@ -129,7 +132,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): return True return False - ## Set all the interface elements and texts for this output device. + ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'. self.setName(self._id) @@ -137,6 +140,32 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) + ## Called when the network data should be updated. + def _update(self) -> None: + super()._update() + if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL: + return # Avoid calling the cloud too often + + Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL) + if self._account.isLoggedIn: + self.setAuthenticationState(AuthState.Authenticated) + self._last_request_time = time() + self._api.getClusterStatus(self.key, self._onStatusCallFinished) + else: + self.setAuthenticationState(AuthState.NotAuthenticated) + + ## Method called when HTTP request to status endpoint is finished. + # Contains both printers and print jobs statuses in a single response. + def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: + # Update all data from the cluster. + self._last_response_time = time() + if self._received_printers != status.printers: + self._received_printers = status.printers + self._updatePrinters(status.printers) + if status.print_jobs != self._received_print_jobs: + self._received_print_jobs = status.print_jobs + self._updatePrintJobs(status.print_jobs) + ## Called when Cura requests an output device to receive a (G-code) file. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: @@ -159,54 +188,29 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Indicate we have started sending a job. self.writeStarted.emit(self) - mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) - if not mesh_format.is_valid: - Logger.log("e", "Missing file or mesh writer!") - return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job.")) + # Export the scene to the correct file type. + job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion) + job.finished.connect(self._onPrintJobCreated) + job.start() - # TODO: use stream just like the network output device - mesh = mesh_format.getBytes(nodes) - self._tool_path = mesh + ## Handler for when the print job was created locally. + # It can now be sent over the cloud. + def _onPrintJobCreated(self, job: WriteFileJob) -> None: + self._progress.show() + self._tool_path = job.getOutput() request = CloudPrintJobUploadRequest( - job_name=file_name or mesh_format.file_extension, - file_size=len(mesh), - content_type=mesh_format.mime_type, + job_name=job.getFileName(), + file_size=len(self._tool_path), + content_type=job.getMimeType(), ) - self._api.requestUpload(request, self._onPrintJobCreated) - - ## Called when the network data should be updated. - def _update(self) -> None: - super()._update() - if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL: - return # Avoid calling the cloud too often - - Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL) - if self._account.isLoggedIn: - self.setAuthenticationState(AuthState.Authenticated) - self._last_request_time = time() - self._api.getClusterStatus(self.key, self._onStatusCallFinished) - else: - self.setAuthenticationState(AuthState.NotAuthenticated) - - ## Method called when HTTP request to status endpoint is finished. - # Contains both printers and print jobs statuses in a single response. - def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: - # Update all data from the cluster. - self._last_response_time = time() - if self._received_printers != status.printers: - self._received_printers = status.printers - self._updatePrinters(status.printers) - if status.print_jobs != self._received_print_jobs: - self._received_print_jobs = status.print_jobs - self._updatePrintJobs(status.print_jobs) + self._api.requestUpload(request, self._uploadPrintJob) ## Uploads the mesh when the print job was registered with the cloud API. # \param job_response: The response received from the cloud API. - def _onPrintJobCreated(self, job_response: CloudPrintJobResponse) -> None: + def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None: self._progress.show() - self._uploaded_print_job = job_response - tool_path = cast(bytes, self._tool_path) - self._api.uploadToolPath(job_response, tool_path, self._onPrintJobUploaded, self._progress.update, + self._uploaded_print_job = job_response # store the last uploaded job to prevent re-upload of the same file + self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update, self._onUploadError) ## Requests the print to be sent to the printer when we finished uploading the mesh. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 3ebe8ac90f..6e8700b2e0 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -40,6 +40,7 @@ class CloudOutputDeviceManager: self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._api = CloudApiClient(self._account, self._onApiError) + self._account.loginStateChanged.connect(self._onLoginStateChanged) # Create a timer to update the remote cluster list self._update_timer = QTimer() @@ -53,17 +54,22 @@ class CloudOutputDeviceManager: def start(self): if self._running: return - self._account.loginStateChanged.connect(self._onLoginStateChanged) + if not self._account.isLoggedIn: + return + self._running = True + if not self._update_timer.isActive(): + self._update_timer.start() self._update_timer.timeout.connect(self._getRemoteClusters) - self._onLoginStateChanged(is_logged_in=self._account.isLoggedIn) ## Stops running the cloud output device manager. def stop(self): if not self._running: return - self._account.loginStateChanged.disconnect(self._onLoginStateChanged) + self._running = False + if self._update_timer.isActive(): + self._update_timer.stop() + self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices. self._update_timer.timeout.disconnect(self._getRemoteClusters) - self._onLoginStateChanged(is_logged_in=False) ## Force refreshing connections. def refreshConnections(self) -> None: @@ -72,17 +78,13 @@ class CloudOutputDeviceManager: ## Called when the uses logs in or out def _onLoginStateChanged(self, is_logged_in: bool) -> None: if is_logged_in: - if not self._update_timer.isActive(): - self._update_timer.start() - self._getRemoteClusters() + self.start() else: - if self._update_timer.isActive(): - self._update_timer.stop() - # Notify that all clusters have disappeared - self._onGetRemoteClustersFinished([]) + self.stop() ## Gets all remote clusters from the API. def _getRemoteClusters(self) -> None: + print("getRemoteClusters") self._api.getClusters(self._onGetRemoteClustersFinished) ## Callback for when the request for getting the clusters is finished. @@ -113,6 +115,8 @@ class CloudOutputDeviceManager: keys = self._remote_clusters.keys() removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys] for device in removed_devices: + device.disconnect() + device.close() CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) discovery.removeDiscoveredPrinter(device.key) @@ -127,11 +131,13 @@ class CloudOutputDeviceManager: return # The newly added machine is automatically activated. - CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, - device.clusterData.friendly_name) + machine_manager = CuraApplication.getInstance().getMachineManager() + machine_manager.addMachine(device.printerType, device.clusterData.friendly_name) active_machine = CuraApplication.getInstance().getGlobalContainerStack() - if active_machine: - self._connectToOutputDevice(device, active_machine) + 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: @@ -150,28 +156,25 @@ class CloudOutputDeviceManager: device = self._remote_clusters[stored_cluster_id] self._connectToOutputDevice(device, active_machine) Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id) - else: - self._connectByNetworkKey(active_machine) + # else: + # self._connectByNetworkKey(active_machine) ## Tries to match the local network key to the cloud cluster host name. def _connectByNetworkKey(self, active_machine: GlobalStack) -> None: - # Check if the active printer has a local network connection and match this key to the remote cluster. local_network_key = active_machine.getMetaDataEntry("um_network_key") if not local_network_key: return - device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None) if not device: return - Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) self._connectToOutputDevice(device, active_machine) ## Connects to an output device and makes sure it is registered in the output device manager. - def _connectToOutputDevice(self, device: CloudOutputDevice, active_machine: GlobalStack) -> None: + @staticmethod + def _connectToOutputDevice(device: CloudOutputDevice, active_machine: GlobalStack) -> None: device.connect() - active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) active_machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/ExportFileJob.py b/plugins/UM3NetworkPrinting/src/ExportFileJob.py new file mode 100644 index 0000000000..605fa054bd --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/ExportFileJob.py @@ -0,0 +1,39 @@ +from typing import List + +from UM.FileHandler.FileHandler import FileHandler +from UM.FileHandler.WriteFileJob import WriteFileJob +from UM.Logger import Logger +from UM.Scene.SceneNode import SceneNode +from cura.CuraApplication import CuraApplication + +from .MeshFormatHandler import MeshFormatHandler + + +## Job that exports the build plate to the correct file format for the target cluster. +class ExportFileJob(WriteFileJob): + + def __init__(self, file_handler: FileHandler, nodes: List[SceneNode], firmware_version: str) -> None: + + self._mesh_format_handler = MeshFormatHandler(file_handler, firmware_version) + if not self._mesh_format_handler.is_valid: + Logger.log("e", "Missing file or mesh writer!") + return + + # Determine the filename. + job_name = CuraApplication.getInstance().getPrintInformation().jobName + extension = self._mesh_format_handler.preferred_format.get("extension", "") + self.setFileName(f"{job_name}.{extension}") + + super().__init__(self._mesh_format_handler.writer, self._mesh_format_handler.createStream(), nodes, + self._mesh_format_handler.file_mode) + + ## Get the mime type of the selected export file type. + def getMimeType(self) -> str: + return self._mesh_format_handler.mime_type + + ## Get the job result as bytes as that is what we need to upload to the cluster. + def getOutput(self) -> bytes: + output = self.getStream().getvalue() + if isinstance(output, str): + output = output.encode("utf-8") + return output diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 7249683f4b..96cfab6888 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -15,6 +15,7 @@ from UM.Scene.SceneNode import SceneNode from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob from .ClusterApiClient import ClusterApiClient from ..MeshFormatHandler import MeshFormatHandler @@ -46,7 +47,6 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._setInterfaceElements() self._active_camera_url = QUrl() # type: QUrl - ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: self.setPriority(3) # Make sure the output device gets selected above local file output @@ -146,21 +146,8 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Make sure the printer is aware of all new materials as the new print job might contain one. self.sendMaterialProfiles() - # Detect the correct export format depending on printer type and firmware version. - mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) - if not mesh_format.is_valid: - Logger.log("e", "Missing file or mesh writer!") - return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job.")) - - # Determine the filename. - job_name = CuraApplication.getInstance().getPrintInformation().jobName - extension = mesh_format.preferred_format.get("extension", "") - file_name = f"{job_name}.{extension}" - - # Export the file. - stream = mesh_format.createStream() - job = WriteFileJob(writer=mesh_format.writer, stream=stream, data=nodes, mode=mesh_format.file_mode) - job.setFileName(file_name) + # Export the scene to the correct file type. + job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion) job.finished.connect(self._onPrintJobCreated) job.start() @@ -168,14 +155,10 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # It can now be sent over the network. def _onPrintJobCreated(self, job: WriteFileJob) -> None: self._progress.show() - # TODO: extract multi-part stuff - parts = [] - parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) - output = job.getStream().getvalue() - if isinstance(output, str): - # Ensure that our output is bytes - output = output.encode("utf-8") - parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), output)) + parts = [ + self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"), + self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), job.getOutput()) + ] self.postFormWithParts("/cluster-api/v1/print_jobs/", parts, on_finished=self._onPrintUploadCompleted, on_progress=self._onPrintJobUploadProgress) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 6bed02549f..9544435185 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -118,8 +118,6 @@ class NetworkOutputDeviceManager: @staticmethod def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None: device.connect() - active_machine.setMetaDataEntry("um_network_key", device.key) - active_machine.setMetaDataEntry("group_name", device.name) active_machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) @@ -199,6 +197,8 @@ class NetworkOutputDeviceManager: # The newly added machine is automatically activated. CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name) active_machine = CuraApplication.getInstance().getGlobalContainerStack() + active_machine.setMetaDataEntry("um_network_key", device.key) + active_machine.setMetaDataEntry("group_name", device.name) if active_machine: self._connectToOutputDevice(device, active_machine) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 18ee65b66f..42529c1df7 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -14,8 +14,6 @@ from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager ## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing. class UM3OutputDevicePlugin(OutputDevicePlugin): - # cloudFlowIsPossible = Signal() - def __init__(self) -> None: super().__init__() @@ -29,24 +27,6 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # This ensures no output devices are still connected that do not belong to the new active machine. CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) - # TODO: re-write cloud messaging - # self._account = self._application.getCuraAPI().account - - # Check if cloud flow is possible when user logs in - # self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible) - - # Check if cloud flow is possible when user switches machines - # self._application.globalContainerStackChanged.connect(self._onMachineSwitched) - - # Listen for when cloud flow is possible - # self.cloudFlowIsPossible.connect(self._onCloudFlowPossible) - - # self._start_cloud_flow_message = None # type: Optional[Message] - # self._cloud_flow_complete_message = None # type: Optional[Message] - - # self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured) - # self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible) - ## Start looking for devices in the network and cloud. def start(self): self._network_output_device_manager.start() From 1ec2ac411888c2a01c6a5bd88c7a91df1ae7b940 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 15:20:59 +0200 Subject: [PATCH 25/52] some cleanup --- .../src/Cloud/CloudOutputDevice.py | 6 ++---- .../src/Cloud/CloudOutputDeviceManager.py | 13 +++++++------ plugins/UM3NetworkPrinting/src/MeshFormatHandler.py | 7 +++---- .../Http/ClusterPrinterConfigurationMaterial.py | 5 +---- .../src/Network/NetworkOutputDevice.py | 5 ++--- .../src/UltimakerNetworkedPrinterOutputDevice.py | 7 ------- 6 files changed, 15 insertions(+), 28 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 446a8bb73b..ee5e39cdaa 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -17,11 +17,10 @@ from UM.Version import Version from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob from .CloudApiClient import CloudApiClient +from ..ExportFileJob import ExportFileJob from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice -from ..MeshFormatHandler import MeshFormatHandler from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterStatus import CloudClusterStatus from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest @@ -106,6 +105,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): super().connect() Logger.log("i", "Connected to cluster %s", self.key) CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange) + self._update() ## Disconnects the device def disconnect(self) -> None: @@ -145,8 +145,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): super()._update() if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL: return # Avoid calling the cloud too often - - Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL) if self._account.isLoggedIn: self.setAuthenticationState(AuthState.Authenticated) self._last_request_time = time() diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 6e8700b2e0..1b1236c719 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -59,6 +59,7 @@ class CloudOutputDeviceManager: self._running = True if not self._update_timer.isActive(): self._update_timer.start() + self._getRemoteClusters() self._update_timer.timeout.connect(self._getRemoteClusters) ## Stops running the cloud output device manager. @@ -84,7 +85,6 @@ class CloudOutputDeviceManager: ## Gets all remote clusters from the API. def _getRemoteClusters(self) -> None: - print("getRemoteClusters") self._api.getClusters(self._onGetRemoteClustersFinished) ## Callback for when the request for getting the clusters is finished. @@ -112,7 +112,7 @@ class CloudOutputDeviceManager: new_devices[device.key] = device # Remove output devices that disappeared. - keys = self._remote_clusters.keys() + keys = new_devices.keys() removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys] for device in removed_devices: device.disconnect() @@ -156,8 +156,8 @@ class CloudOutputDeviceManager: device = self._remote_clusters[stored_cluster_id] self._connectToOutputDevice(device, active_machine) Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id) - # else: - # self._connectByNetworkKey(active_machine) + else: + self._connectByNetworkKey(active_machine) ## Tries to match the local network key to the cloud cluster host name. def _connectByNetworkKey(self, active_machine: GlobalStack) -> None: @@ -167,9 +167,10 @@ class CloudOutputDeviceManager: device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None) if not device: return + print("CONNECT BY NETWORK KEY", local_network_key, device.key, device.name) Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) - active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) - self._connectToOutputDevice(device, active_machine) + # active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + # self._connectToOutputDevice(device, active_machine) ## Connects to an output device and makes sure it is registered in the output device manager. @staticmethod diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 2f01743dd6..1e76c22a50 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -91,10 +91,9 @@ class MeshFormatHandler: machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";") machine_file_formats = [file_type.strip() for file_type in machine_file_formats] - # TODO: re-enable UFP after Cura master branch works again - # # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. - # if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"): - # machine_file_formats = ["application/x-ufp"] + machine_file_formats + # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. + if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"): + machine_file_formats = ["application/x-ufp"] + machine_file_formats # Take the intersection between file_formats and machine_file_formats. format_by_mimetype = {f["mime_type"]: f for f in file_formats} diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index 57e763e159..68c83ba76b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -2,7 +2,6 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional -from UM.Logger import Logger from cura.CuraApplication import CuraApplication from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel @@ -48,11 +47,9 @@ class ClusterPrinterConfigurationMaterial(BaseModel): material_type = container.getMetaDataEntry("material") name = container.getName() else: - Logger.log("w", "Unable to find material with guid {guid}. Using data as provided by cluster" - .format(guid = self.guid)) color = self.color brand = self.brand material_type = self.material name = "Empty" if self.material == "empty" else "Unknown" - return MaterialOutputModel(guid = self.guid, type = material_type, brand = brand, color = color, name = name) + return MaterialOutputModel(guid=self.guid, type=material_type, brand=brand, color=color, name=name) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 96cfab6888..6be105a6a2 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -12,13 +12,11 @@ from UM.Logger import Logger from UM.Message import Message from UM.i18n import i18nCatalog from UM.Scene.SceneNode import SceneNode -from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob from .ClusterApiClient import ClusterApiClient -from ..MeshFormatHandler import MeshFormatHandler +from ..ExportFileJob import ExportFileJob from ..SendMaterialJob import SendMaterialJob from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice @@ -58,6 +56,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Called when the connection to the cluster changes. def connect(self) -> None: super().connect() + self._update() self.sendMaterialProfiles() @pyqtProperty(QUrl, notify=activeCameraUrlChanged) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index ee6a67992f..fedead7bce 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -241,7 +241,6 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): for removed_job in removed_jobs: if removed_job.assignedPrinter: removed_job.assignedPrinter.updateActivePrintJob(None) - removed_job.stateChanged.disconnect(self._onPrintJobStateChanged) self._print_jobs = new_print_jobs self.printJobsChanged.emit() @@ -250,20 +249,14 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # \param remote_job: The remote print job data. def _createPrintJobModel(self, remote_job: ClusterPrintJobStatus) -> UM3PrintJobOutputModel: model = remote_job.createOutputModel(ClusterOutputController(self)) - model.stateChanged.connect(self._onPrintJobStateChanged) if remote_job.printer_uuid: self._updateAssignedPrinter(model, remote_job.printer_uuid) return model - def _onPrintJobStateChanged(self) -> None: - pass - ## Updates the printer assignment for the given print job model. def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: printer = next((p for p in self._printers if printer_uuid == p.key), None) if not printer: - Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key, - [p.key for p in self._printers]) return printer.updateActivePrintJob(model) model.updateAssignedPrinter(printer) From 7398f08b27d285e6d582c104b1664497286912f2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 15:30:34 +0200 Subject: [PATCH 26/52] Fix job name --- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 1 - plugins/UM3NetworkPrinting/src/ExportFileJob.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index ee5e39cdaa..3f97d74068 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -194,7 +194,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Handler for when the print job was created locally. # It can now be sent over the cloud. def _onPrintJobCreated(self, job: WriteFileJob) -> None: - self._progress.show() self._tool_path = job.getOutput() request = CloudPrintJobUploadRequest( job_name=job.getFileName(), diff --git a/plugins/UM3NetworkPrinting/src/ExportFileJob.py b/plugins/UM3NetworkPrinting/src/ExportFileJob.py index 605fa054bd..fdbcca00be 100644 --- a/plugins/UM3NetworkPrinting/src/ExportFileJob.py +++ b/plugins/UM3NetworkPrinting/src/ExportFileJob.py @@ -19,14 +19,14 @@ class ExportFileJob(WriteFileJob): Logger.log("e", "Missing file or mesh writer!") return + super().__init__(self._mesh_format_handler.writer, self._mesh_format_handler.createStream(), nodes, + self._mesh_format_handler.file_mode) + # Determine the filename. job_name = CuraApplication.getInstance().getPrintInformation().jobName extension = self._mesh_format_handler.preferred_format.get("extension", "") self.setFileName(f"{job_name}.{extension}") - super().__init__(self._mesh_format_handler.writer, self._mesh_format_handler.createStream(), nodes, - self._mesh_format_handler.file_mode) - ## Get the mime type of the selected export file type. def getMimeType(self) -> str: return self._mesh_format_handler.mime_type From 64c7b2e7379d400cf564362e03237aa1294fd5bb Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 15:32:34 +0200 Subject: [PATCH 27/52] Remove some TODOs --- .../src/Models/Http/CloudPrintJobResponse.py | 1 - plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py index 7c056fcb5e..d88139acee 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py @@ -28,6 +28,5 @@ class CloudPrintJobResponse(BaseModel): self.upload_url = upload_url self.content_type = content_type self.status_description = status_description - # TODO: Implement slicing details self.slicing_details = slicing_details super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 3e3dbed46d..df0dcad527 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -59,9 +59,6 @@ class ClusterApiClient: reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrintJobStatus) - def requestPrint(self) -> None: - pass # TODO - ## Move a print job to the top of the queue. def movePrintJobToTop(self, print_job_uuid: str) -> None: url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action/move" @@ -79,9 +76,6 @@ class ClusterApiClient: action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) - def forcePrintJob(self, print_job_uuid: str) -> None: - pass # TODO - ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. From cf1dff3fdc3efb6974ba27bc587ffac883c06ddb Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 15:51:56 +0200 Subject: [PATCH 28/52] Fix camera stream --- .../UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py | 3 +++ plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 43aa714521..dcdca5a017 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -2,6 +2,8 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import List, Union, Dict, Optional, Any +from PyQt5.QtCore import QUrl + from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from .ClusterBuildPlate import ClusterBuildPlate @@ -66,6 +68,7 @@ class ClusterPrinterStatus(BaseModel): model.updateType(self.machine_variant) model.updateState(self.status if self.enabled else "disabled") model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") + model.setCameraUrl(QUrl(f"http://{self.ip_address}:8080/?action=stream")) for configuration, extruder_output, extruder_config in \ zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 6be105a6a2..e4e9a02062 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -87,7 +87,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: - self._cluster_api.forcePrintJob(print_job_uuid) + pass # TODO ## Set the remote print job state. # \param print_job_uuid: The UUID of the print job to set the state for. From 69102643b758a94c0293e3b608ad365be733027c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 16:55:35 +0200 Subject: [PATCH 29/52] Bring back print job preview images --- .../src/Models/Http/ClusterPrintJobStatus.py | 2 ++ .../src/Models/Http/ClusterPrinterStatus.py | 1 + .../src/Models/UM3PrintJobOutputModel.py | 6 +++++ .../src/Network/ClusterApiClient.py | 26 ++++++++++++++----- .../src/Network/NetworkOutputDevice.py | 25 +++++++----------- .../UltimakerNetworkedPrinterOutputDevice.py | 1 + 6 files changed, 38 insertions(+), 23 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index 86bf02f9dd..ea2adf4b1b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -2,6 +2,8 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import List, Optional, Union, Dict, Any +from PyQt5.QtCore import QUrl + from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from .ClusterBuildPlate import ClusterBuildPlate diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index dcdca5a017..ad5d117ded 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -6,6 +6,7 @@ from PyQt5.QtCore import QUrl from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel + from .ClusterBuildPlate import ClusterBuildPlate from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration from ..BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py index 1a27fc3f73..a9099a21aa 100644 --- a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py @@ -3,6 +3,7 @@ from typing import List from PyQt5.QtCore import pyqtProperty, pyqtSignal +from PyQt5.QtGui import QImage from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController @@ -26,3 +27,8 @@ class UM3PrintJobOutputModel(PrintJobOutputModel): return self._configuration_changes = changes self.configurationChangesChanged.emit() + + def updatePreviewImageData(self, data: bytes) -> None: + image = QImage() + image.loadFromData(data) + self.updatePreviewImage(image) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index df0dcad527..d3a3e61367 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -76,6 +76,12 @@ class ClusterApiClient: action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) + ## Get the preview image data of a print job. + def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None: + url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/preview_image" + reply = self._manager.get(self._createEmptyRequest(url)) + self._addCallback(reply, on_finished) + ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. @@ -124,17 +130,23 @@ class ClusterApiClient: reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], - model: Type[ClusterApiClientModel], + model: Optional[Type[ClusterApiClientModel]] = None, ) -> None: def parse() -> None: # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: return - status_code, response = self._parseReply(reply) + self._anti_gc_callbacks.remove(parse) - if on_finished: - self._parseModels(response, on_finished, model) - return + + # If no parse model is given, simply return the raw data in the callback. + if not model: + on_finished(reply.readAll()) + return + + # Otherwise parse the result and return the formatted data in the callback. + status_code, response = self._parseReply(reply) + self._parseModels(response, on_finished, model) + self._anti_gc_callbacks.append(parse) - if on_finished: - reply.finished.connect(parse) + reply.finished.connect(parse) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index e4e9a02062..74f68c0a5e 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -2,7 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional, Dict, List, Any -from PyQt5.QtGui import QDesktopServices +from PyQt5.QtGui import QDesktopServices, QImage from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty from PyQt5.QtNetwork import QNetworkReply @@ -14,6 +14,7 @@ from UM.i18n import i18nCatalog from UM.Scene.SceneNode import SceneNode from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType +from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from .ClusterApiClient import ClusterApiClient from ..ExportFileJob import ExportFileJob @@ -104,9 +105,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): super()._update() self._cluster_api.getPrinters(self._updatePrinters) self._cluster_api.getPrintJobs(self._updatePrintJobs) - # for print_job in self._print_jobs: - # if print_job.getPreviewImage() is None: - # self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) + self._updatePrintJobPreviewImages() ## Sync the material profiles in Cura with the printer. # This gets called when connecting to a printer as well as when sending a print. @@ -114,18 +113,6 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): job = SendMaterialJob(device=self) job.run() - # ## Callback for when preview image was downloaded from cluster. - # def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: - # reply_url = reply.url().toString() - # - # uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")] - # - # print_job = findByKey(self._print_jobs, uuid) - # if print_job: - # image = QImage() - # image.loadFromData(reply.readAll()) - # print_job.updatePreviewImage(image) - ## Send a print job to the cluster. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: @@ -187,3 +174,9 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): lifetime=10 ).show() self.writeError.emit() + + ## Download all the images from the cluster and load their data in the print job models. + def _updatePrintJobPreviewImages(self): + for print_job in self._print_jobs: + if print_job.getPreviewImage() is None: + self._cluster_api.getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index fedead7bce..2bfacdbb25 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -4,6 +4,7 @@ import os from typing import List, Optional, Dict from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot, QUrl +from PyQt5.QtGui import QImage from UM.Logger import Logger from UM.Qt.Duration import Duration, DurationFormat From aa6105f1be77139e3c6aece964f81e26024ec5d8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 16:56:58 +0200 Subject: [PATCH 30/52] cleanup imports --- plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index 74f68c0a5e..e109904046 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -14,7 +14,6 @@ from UM.i18n import i18nCatalog from UM.Scene.SceneNode import SceneNode from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.PrinterOutputDevice import ConnectionType -from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel from .ClusterApiClient import ClusterApiClient from ..ExportFileJob import ExportFileJob From c0933ddb2d56363e42a7f4dd377d1668866d287f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 17:00:30 +0200 Subject: [PATCH 31/52] Simplify return --- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 6 ++---- .../UM3NetworkPrinting/src/Network/NetworkOutputDevice.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 3f97d74068..14c17c33a3 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -170,13 +170,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Show an error message if we're already sending a job. if self._progress.visible: - message = Message( + return Message( text=I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), title=I18N_CATALOG.i18nc("@info:title", "Print error"), lifetime=10 - ) - message.show() - return + ).show() if self._uploaded_print_job: # The mesh didn't change, let's not upload it again diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index e109904046..d138d78beb 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -118,13 +118,11 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Show an error message if we're already sending a job. if self._progress.visible: - message = Message( + return Message( text=I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), title=I18N_CATALOG.i18nc("@info:title", "Print error"), lifetime=10 - ) - message.show() - return + ).show() self.writeStarted.emit(self) From b7dfa11e690a64ccaeba837df75b71261c4e3ca8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 17:41:03 +0200 Subject: [PATCH 32/52] Enable network key pairing again --- .../UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 1b1236c719..206fdb0495 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -167,10 +167,9 @@ class CloudOutputDeviceManager: device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None) if not device: return - print("CONNECT BY NETWORK KEY", local_network_key, device.key, device.name) Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) - # active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) - # self._connectToOutputDevice(device, active_machine) + active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + self._connectToOutputDevice(device, active_machine) ## Connects to an output device and makes sure it is registered in the output device manager. @staticmethod From c1b5cce064113f10f7cef62b648b40870fcea448 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 18:04:16 +0200 Subject: [PATCH 33/52] Fix string formatting --- .../resources/qml/MonitorPrintJobPreview.qml | 4 ++-- .../src/Cloud/CloudOutputDeviceManager.py | 5 +++-- plugins/UM3NetworkPrinting/src/ExportFileJob.py | 2 +- .../src/Models/Http/ClusterPrinterStatus.py | 2 +- .../src/Network/ClusterApiClient.py | 14 +++++++------- .../src/Network/NetworkOutputDeviceManager.py | 4 ++-- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml index a392571757..d8749f18b6 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml @@ -41,7 +41,7 @@ Item UM.RecolorImage { id: ultiBotImage - + anchors.centerIn: printJobPreview color: UM.Theme.getColor("monitor_placeholder_image") height: printJobPreview.height @@ -98,4 +98,4 @@ Item visible: source != "" width: 0.5 * printJobPreview.width } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 206fdb0495..64562d3872 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -168,8 +168,9 @@ class CloudOutputDeviceManager: if not device: return Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) - active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) - self._connectToOutputDevice(device, active_machine) + # TODO: fix this + # active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + # self._connectToOutputDevice(device, active_machine) ## Connects to an output device and makes sure it is registered in the output device manager. @staticmethod diff --git a/plugins/UM3NetworkPrinting/src/ExportFileJob.py b/plugins/UM3NetworkPrinting/src/ExportFileJob.py index fdbcca00be..7abe1e6d8e 100644 --- a/plugins/UM3NetworkPrinting/src/ExportFileJob.py +++ b/plugins/UM3NetworkPrinting/src/ExportFileJob.py @@ -25,7 +25,7 @@ class ExportFileJob(WriteFileJob): # Determine the filename. job_name = CuraApplication.getInstance().getPrintInformation().jobName extension = self._mesh_format_handler.preferred_format.get("extension", "") - self.setFileName(f"{job_name}.{extension}") + self.setFileName("{}.{}".format(job_name, extension)) ## Get the mime type of the selected export file type. def getMimeType(self) -> str: diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index ad5d117ded..c7aaea39bc 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -69,7 +69,7 @@ class ClusterPrinterStatus(BaseModel): model.updateType(self.machine_variant) model.updateState(self.status if self.enabled else "disabled") model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") - model.setCameraUrl(QUrl(f"http://{self.ip_address}:8080/?action=stream")) + model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) for configuration, extruder_output, extruder_config in \ zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index d3a3e61367..88383a13cf 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -42,43 +42,43 @@ class ClusterApiClient: ## Get printer system information. # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: - url = f"{self.PRINTER_API_PREFIX}/system/" + url = "{}/system/".format(self.PRINTER_API_PREFIX) self._manager.get(self._createEmptyRequest(url)) ## Get the printers in the cluster. # \param on_finished: The callback in case the response is successful. def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: - url = f"{self.CLUSTER_API_PREFIX}/printers/" + url = "{}/printers/".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrinterStatus) ## Get the print jobs in the cluster. # \param on_finished: The callback in case the response is successful. def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: - url = f"{self.CLUSTER_API_PREFIX}/print_jobs/" + url = "{}/print_jobs/".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrintJobStatus) ## Move a print job to the top of the queue. def movePrintJobToTop(self, print_job_uuid: str) -> None: - url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action/move" + url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode()) ## Delete a print job from the queue. def deletePrintJob(self, print_job_uuid: str) -> None: - url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}" + url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.deleteResource(self._createEmptyRequest(url)) ## Set the state of a print job. def setPrintJobState(self, print_job_uuid: str, state: str) -> None: - url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/action" + url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid) # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) ## Get the preview image data of a print job. def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None: - url = f"{self.CLUSTER_API_PREFIX}/print_jobs/{print_job_uuid}/preview_image" + url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 9544435185..c406437384 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -63,7 +63,7 @@ class NetworkOutputDeviceManager: new_manual_devices = ",".join(self._manual_instances.keys()) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) - device_id = f"manual:{address}" + device_id = "manual:{}".format(address) if device_id not in self._discovered_devices: self._onDeviceDiscovered(device_id, address, { b"name": address.encode("utf-8"), @@ -79,7 +79,7 @@ class NetworkOutputDeviceManager: ## Remove a manually added networked printer. def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: if device_id not in self._discovered_devices and address is not None: - device_id = f"manual:{address}" + device_id = "manual:{}".format(address) if device_id in self._discovered_devices: address = address or self._discovered_devices[device_id].ipAddress From 72ac8b5f4c8b938d84ef4b17c51729ba582cbc97 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 19:29:35 +0200 Subject: [PATCH 34/52] Simplify output device cleanup code --- .../src/Cloud/CloudOutputDeviceManager.py | 41 +++++++------------ .../src/Network/NetworkOutputDeviceManager.py | 34 ++++++++------- 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 64562d3872..a05f0794eb 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -25,6 +25,7 @@ from ..Models.Http.CloudError import CloudError class CloudOutputDeviceManager: META_CLUSTER_ID = "um_cloud_cluster_id" + META_NETWORK_KEY = "um_network_key" # The interval with which the remote clusters are checked CHECK_CLUSTER_INTERVAL = 30.0 # seconds @@ -125,9 +126,8 @@ class CloudOutputDeviceManager: self._connectToActiveMachine() def _createMachineFromDiscoveredDevice(self, key: str) -> None: - device = self._remote_clusters[key] # type: CloudOutputDevice + device = self._remote_clusters[key] if not device: - Logger.log("e", "Could not find discovered device with key [%s]", key) return # The newly added machine is automatically activated. @@ -145,32 +145,19 @@ class CloudOutputDeviceManager: if not active_machine: return - # Remove all output devices that we have registered. - # This is needed because when we switch we can only leave output devices that are meant for that machine. - for device_id in self._remote_clusters: - CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device_id) - - # Check if the stored cluster_id for the active machine is in our list of remote clusters. stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) - if stored_cluster_id in self._remote_clusters: - device = self._remote_clusters[stored_cluster_id] - self._connectToOutputDevice(device, active_machine) - Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id) - else: - self._connectByNetworkKey(active_machine) - - ## Tries to match the local network key to the cloud cluster host name. - def _connectByNetworkKey(self, active_machine: GlobalStack) -> None: - local_network_key = active_machine.getMetaDataEntry("um_network_key") - if not local_network_key: - return - device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None) - if not device: - return - Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) - # TODO: fix this - # active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) - # self._connectToOutputDevice(device, active_machine) + local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) + for device in self._remote_clusters.values(): + if device.key == stored_cluster_id: + # Connect to it if the stored ID matches. + self._connectToOutputDevice(device, active_machine) + elif local_network_key and device.matchesNetworkKey(local_network_key): + # Connect to it if we can match the local network key that was already present. + active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + self._connectToOutputDevice(device, active_machine) + else: + # Remove device if it is not meant for the active machine. + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) ## Connects to an output device and makes sure it is registered in the output device manager. @staticmethod diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index c406437384..96c6531a35 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -18,6 +18,8 @@ from .NetworkOutputDevice import NetworkOutputDevice ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. class NetworkOutputDeviceManager: + + META_NETWORK_KEY = "um_network_key" MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0") @@ -72,9 +74,8 @@ class NetworkOutputDeviceManager: b"incomplete": b"true", b"temporary": b"true" }) - - response_callback = lambda status_code, response: self._onCheckManualDeviceResponse(status_code, address) - self._checkManualDevice(address, response_callback) + self._checkManualDevice(address, lambda status_code, response: self._onCheckManualDeviceResponse( + status_code, address)) ## Remove a manually added networked printer. def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: @@ -102,17 +103,14 @@ class NetworkOutputDeviceManager: if not active_machine: return - # Remove all output devices that we have registered. - # This is needed because when we switch we can only leave output devices that are meant for that machine. - for device_id in self._discovered_devices: - CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device_id) - - # Check if the stored network key for the active machine is in our list of discovered devices. - stored_network_key = active_machine.getMetaDataEntry("um_network_key") - if stored_network_key in self._discovered_devices: - device = self._discovered_devices[stored_network_key] - self._connectToOutputDevice(device, active_machine) - Logger.log("d", "Device connected by metadata network key %s", stored_network_key) + stored_device_id = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) + for device in self._discovered_devices.values(): + if device.key == stored_device_id: + # Connect to it if the stored key matches. + self._connectToOutputDevice(device, active_machine) + else: + # Remove device if it is not meant for the active machine. + CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) ## Add a device to the current active machine. @staticmethod @@ -191,16 +189,16 @@ class NetworkOutputDeviceManager: def _createMachineFromDiscoveredDevice(self, device_id: str) -> None: device = self._discovered_devices.get(device_id) if device is None: - Logger.log("e", "Could not find discovered device with device_id [%s]", device_id) return # The newly added machine is automatically activated. CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name) active_machine = CuraApplication.getInstance().getGlobalContainerStack() - active_machine.setMetaDataEntry("um_network_key", device.key) + if not active_machine: + return + active_machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) active_machine.setMetaDataEntry("group_name", device.name) - if active_machine: - self._connectToOutputDevice(device, active_machine) + self._connectToOutputDevice(device, active_machine) ## Load the user-configured manual devices from Cura preferences. def _getStoredManualInstances(self) -> Dict[str, Optional[Callable]]: From d28025243765f79ada78987a5cbd99f941907985 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 22:21:36 +0200 Subject: [PATCH 35/52] Cleanup --- .../src/Cloud/CloudApiClient.py | 6 +- .../src/Cloud/CloudOutputDeviceManager.py | 85 +++++++++---------- .../src/Network/ClusterApiClient.py | 22 +++-- .../src/Network/NetworkOutputDevice.py | 14 +-- .../src/Network/NetworkOutputDeviceManager.py | 42 ++++----- .../src/UM3OutputDevicePlugin.py | 2 +- .../UltimakerNetworkedPrinterOutputDevice.py | 1 - 7 files changed, 81 insertions(+), 91 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 12079dc497..6b1b4725d0 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -169,14 +169,16 @@ class CloudApiClient: Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], ) -> None: + def parse() -> None: + self._anti_gc_callbacks.remove(parse) + # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: return + status_code, response = self._parseReply(reply) - self._anti_gc_callbacks.remove(parse) self._parseModels(response, on_finished, model) - return self._anti_gc_callbacks.append(parse) reply.finished.connect(parse) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index a05f0794eb..d3acc4037e 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -1,11 +1,10 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Dict, List +from typing import Dict, List, Optional from PyQt5.QtCore import QTimer from UM import i18nCatalog -from UM.Logger import Logger from UM.Signal import Signal from cura.API import Account from cura.CuraApplication import CuraApplication @@ -14,14 +13,11 @@ from cura.Settings.GlobalStack import GlobalStack from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice from ..Models.Http.CloudClusterResponse import CloudClusterResponse -from ..Models.Http.CloudError import CloudError -## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. -# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code. -# -# API spec is available on https://api.ultimaker.com/docs/connect/spec/. -# +## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. +# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code. +# API spec is available on https://api.ultimaker.com/docs/connect/spec/. class CloudOutputDeviceManager: META_CLUSTER_ID = "um_cloud_cluster_id" @@ -40,7 +36,7 @@ class CloudOutputDeviceManager: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._api = CloudApiClient(self._account, self._onApiError) + self._api = CloudApiClient(self._account, on_error=lambda error: print(error)) self._account.loginStateChanged.connect(self._onLoginStateChanged) # Create a timer to update the remote cluster list @@ -90,41 +86,51 @@ class CloudOutputDeviceManager: ## Callback for when the request for getting the clusters is finished. def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: - - # Filter on clusters that are currently online. online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] - - # Keep track of the new cloud clusters to show. - # We create a new list instead of changing the existing one to prevent issues with ordering. - new_devices = {} # type: Dict[str, CloudOutputDevice] - - # Get the discovery mechanism of Cura. - discovery = CuraApplication.getInstance().getDiscoveredPrintersModel() - - # Check which devices need to be created or updated. for device_id, cluster_data in online_clusters.items(): - device = next(iter(device for device in self._remote_clusters.values() if device.key == device_id), None) - if not device: - device = CloudOutputDevice(self._api, cluster_data) - discovery.addDiscoveredPrinter(device.key, device.key, cluster_data.friendly_name, - self._createMachineFromDiscoveredDevice, device.printerType, device) + if device_id not in self._remote_clusters: + self._onDeviceDiscovered(cluster_data) else: - discovery.updateDiscoveredPrinter(device.key, cluster_data.friendly_name, device.printerType) - new_devices[device.key] = device + self._onDiscoveredDeviceUpdated(cluster_data) - # Remove output devices that disappeared. - keys = new_devices.keys() - removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys] - for device in removed_devices: - device.disconnect() - device.close() - CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) - discovery.removeDiscoveredPrinter(device.key) + removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys()) + for device_id in removed_device_keys: + self._onDiscoveredDeviceRemoved(device_id) - self._remote_clusters = new_devices + def _onDeviceDiscovered(self, cluster_data: CloudClusterResponse) -> None: + device = CloudOutputDevice(self._api, cluster_data) + CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( + ip_address=device.key, + key=device.getId(), + name=device.getName(), + create_callback=self._createMachineFromDiscoveredDevice, + machine_type=device.printerType, + device=device + ) + self._remote_clusters[device.getId()] = device self.discoveredDevicesChanged.emit() self._connectToActiveMachine() + def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None: + device = self._remote_clusters.get(cluster_data.cluster_id) + if not device: + return + CuraApplication.getInstance().getDiscoveredPrintersModel().updateDiscoveredPrinter( + ip_address=device.key, + name=cluster_data.friendly_name, + machine_type=device.printerType + ) + self.discoveredDevicesChanged.emit() + + def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: + device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice] + if not device: + return + device.disconnect() + device.close() + CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key) + self.discoveredDevicesChanged.emit() + def _createMachineFromDiscoveredDevice(self, key: str) -> None: device = self._remote_clusters[key] if not device: @@ -165,10 +171,3 @@ class CloudOutputDeviceManager: device.connect() active_machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) - - ## Handles an API error received from the cloud. - # \param errors: The errors received - @staticmethod - def _onApiError(errors: List[CloudError] = None) -> None: - for error in errors: - Logger.log("w", str(error.toDict())) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 88383a13cf..11f9ab033c 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -5,13 +5,14 @@ from json import JSONDecodeError from typing import Callable, List, Optional, Dict, Union, Any, Type, cast, TypeVar, Tuple from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkConfiguration from UM.Logger import Logger from ..Models.BaseModel import BaseModel from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus from ..Models.Http.ClusterPrinterStatus import ClusterPrinterStatus +from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus ## The generic type variable used to document the methods below. @@ -21,11 +22,8 @@ ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel) ## The ClusterApiClient is responsible for all network calls to local network clusters. class ClusterApiClient: - PRINTER_API_VERSION = "1" - PRINTER_API_PREFIX = "/api/v" + PRINTER_API_VERSION - - CLUSTER_API_VERSION = "1" - CLUSTER_API_PREFIX = "/cluster-api/v" + CLUSTER_API_VERSION + PRINTER_API_PREFIX = "/api/v1" + CLUSTER_API_PREFIX = "/cluster-api/v1" ## Initializes a new cluster API client. # \param address: The network address of the cluster to call. @@ -43,7 +41,8 @@ class ClusterApiClient: # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: url = "{}/system/".format(self.PRINTER_API_PREFIX) - self._manager.get(self._createEmptyRequest(url)) + reply = self._manager.get(self._createEmptyRequest(url)) + self._addCallback(reply, on_finished, PrinterSystemStatus) ## Get the printers in the cluster. # \param on_finished: The callback in case the response is successful. @@ -132,12 +131,17 @@ class ClusterApiClient: Callable[[List[ClusterApiClientModel]], Any]], model: Optional[Type[ClusterApiClientModel]] = None, ) -> None: + def parse() -> None: + self._anti_gc_callbacks.remove(parse) + # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: return - - self._anti_gc_callbacks.remove(parse) + + if reply.error() > 0: + self._on_error(reply.errorString()) + return # If no parse model is given, simply return the raw data in the callback. if not model: diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py index d138d78beb..7f6b2f54b3 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py @@ -1,14 +1,13 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict, List, Any +from typing import Optional, Dict, List -from PyQt5.QtGui import QDesktopServices, QImage +from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty from PyQt5.QtNetwork import QNetworkReply from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.WriteFileJob import WriteFileJob -from UM.Logger import Logger from UM.Message import Message from UM.i18n import i18nCatalog from UM.Scene.SceneNode import SceneNode @@ -39,7 +38,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): ) # API client for making requests to the print cluster. - self._cluster_api = ClusterApiClient(address, on_error=self._onNetworkError) + self._cluster_api = ClusterApiClient(address, on_error=lambda error: print(error)) # We don't have authentication over local networking, so we're always authenticated. self.setAuthenticationState(AuthState.Authenticated) self._setInterfaceElements() @@ -95,11 +94,6 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): def setJobState(self, print_job_uuid: str, action: str) -> None: self._cluster_api.setPrintJobState(print_job_uuid, action) - ## Handle network errors. - @staticmethod - def _onNetworkError(errors: Dict[str, Any]): - Logger.log("w", str(errors)) - def _update(self) -> None: super()._update() self._cluster_api.getPrinters(self._updatePrinters) @@ -152,7 +146,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.writeProgress.emit() ## Handler for when the print job was fully uploaded to the cluster. - def _onPrintUploadCompleted(self, reply: QNetworkReply) -> None: + def _onPrintUploadCompleted(self, _: QNetworkReply) -> None: self._progress.hide() Message( text=I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 96c6531a35..72331fdb8d 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -14,6 +14,7 @@ from cura.Settings.GlobalStack import GlobalStack from .ZeroConfClient import ZeroConfClient from .ClusterApiClient import ClusterApiClient from .NetworkOutputDevice import NetworkOutputDevice +from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. @@ -64,18 +65,8 @@ class NetworkOutputDeviceManager: self._manual_instances[address] = callback new_manual_devices = ",".join(self._manual_instances.keys()) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) - - device_id = "manual:{}".format(address) - if device_id not in self._discovered_devices: - self._onDeviceDiscovered(device_id, address, { - b"name": address.encode("utf-8"), - b"address": address.encode("utf-8"), - b"manual": b"true", - b"incomplete": b"true", - b"temporary": b"true" - }) - self._checkManualDevice(address, lambda status_code, response: self._onCheckManualDeviceResponse( - status_code, address)) + api_client = ClusterApiClient(address, self._onApiError) + api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status)) ## Remove a manually added networked printer. def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: @@ -119,19 +110,19 @@ class NetworkOutputDeviceManager: active_machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) - ## Checks if a networked printer exists at the given address. - # If the printer responds it will replace the preliminary printer created from the stored manual instances. - def _checkManualDevice(self, address: str, on_finished: Callable) -> None: - api_client = ClusterApiClient(address, self._onApiError) - api_client.getSystem(on_finished) - ## Callback for when a manual device check request was responded to. - def _onCheckManualDeviceResponse(self, status_code: int, address: str) -> None: - Logger.log("d", "manual device check response: {} {}".format(status_code, address)) - if address in self._manual_instances: - callback = self._manual_instances[address] - if callback is not None: - CuraApplication.getInstance().callLater(callback, status_code == 200, address) + def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus) -> None: + callback = self._manual_instances.get(address, None) + if callback is None: + return + self._onDeviceDiscovered("manual:{}".format(address), address, { + b"name": status.name.encode("utf-8"), + b"address": address.encode("utf-8"), + b"manual": b"true", + b"incomplete": b"true", + b"temporary": b"true" + }) + CuraApplication.getInstance().callLater(callback, True, address) ## Returns a dict of printer BOM numbers to machine types. # These numbers are available in the machine definition already so we just search for them here. @@ -179,9 +170,10 @@ class NetworkOutputDeviceManager: ## Remove a device. def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: - device = self._discovered_devices.pop(device_id, None) + device = self._discovered_devices.pop(device_id, None) # type: Optional[NetworkOutputDevice] if not device: return + device.close() CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) self.discoveredDevicesChanged.emit() diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 42529c1df7..0b108e0a1b 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -44,7 +44,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): ## Indicate that this plugin supports adding networked printers manually. def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt: - return ManualDeviceAdditionAttempt.POSSIBLE + return ManualDeviceAdditionAttempt.PRIORITY ## Add a networked printer manually based on its network address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 2bfacdbb25..fedead7bce 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -4,7 +4,6 @@ import os from typing import List, Optional, Dict from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot, QUrl -from PyQt5.QtGui import QImage from UM.Logger import Logger from UM.Qt.Duration import Duration, DurationFormat From 52a5a43fe289a86c514a117c2f1a50997a848af8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 22:44:45 +0200 Subject: [PATCH 36/52] Show cloud connection flow message when adding local network device --- .../src/CloudFlowMessage.py | 38 +++++ .../src/Models/Http/PrinterSystemStatus.py | 17 +++ .../src/Network/NetworkOutputDeviceManager.py | 24 ++- .../src/UM3OutputDevicePlugin.py | 139 ------------------ 4 files changed, 66 insertions(+), 152 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/CloudFlowMessage.py create mode 100644 plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py diff --git a/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py b/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py new file mode 100644 index 0000000000..ab299afad0 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py @@ -0,0 +1,38 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import os + +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices + +from UM import i18nCatalog +from UM.Message import Message +from cura.CuraApplication import CuraApplication + + +I18N_CATALOG = i18nCatalog("cura") + + +class CloudFlowMessage(Message): + + def __init__(self, address: str) -> None: + super().__init__( + text=I18N_CATALOG.i18nc("@info:status", + "Send and monitor print jobs from anywhere using your Ultimaker account."), + lifetime=0, + dismissable=True, + option_state=False, + image_source=QUrl.fromLocalFile(os.path.join( + CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting"), + "resources", "svg", "cloud-flow-start.svg" + )), + image_caption=I18N_CATALOG.i18nc("@info:status Ultimaker Cloud should not be translated.", + "Connect to Ultimaker Cloud"), + ) + self._address = address + self.addAction("", I18N_CATALOG.i18nc("@action", "Get started"), "", "") + self.actionTriggered.connect(self._onCloudFlowStarted) + + def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None: + QDesktopServices.openUrl(QUrl("http://{}/cloud_connect".format(self._address))) + self.hide() diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py new file mode 100644 index 0000000000..ed85ed1799 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from ..BaseModel import BaseModel + + +## Class representing the system status of a printer. +class PrinterSystemStatus(BaseModel): + + def __init__(self, guid: str, firmware: str, hostname: str, name: str, platform: str, variant: str, **kwargs + ) -> None: + self.guid = guid + self.firmware = firmware + self.hostname = hostname + self.name = name + self.platform = platform + self.variant = variant + super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 72331fdb8d..ea62f2b205 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -3,7 +3,6 @@ from typing import Dict, Optional, Callable from UM import i18nCatalog -from UM.Logger import Logger from UM.Signal import Signal from UM.Version import Version @@ -14,9 +13,13 @@ from cura.Settings.GlobalStack import GlobalStack from .ZeroConfClient import ZeroConfClient from .ClusterApiClient import ClusterApiClient from .NetworkOutputDevice import NetworkOutputDevice +from ..CloudFlowMessage import CloudFlowMessage from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus +I18N_CATALOG = i18nCatalog("cura") + + ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. class NetworkOutputDeviceManager: @@ -65,7 +68,7 @@ class NetworkOutputDeviceManager: self._manual_instances[address] = callback new_manual_devices = ",".join(self._manual_instances.keys()) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices) - api_client = ClusterApiClient(address, self._onApiError) + api_client = ClusterApiClient(address, lambda error: print(error)) api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status)) ## Remove a manually added networked printer. @@ -103,13 +106,6 @@ class NetworkOutputDeviceManager: # Remove device if it is not meant for the active machine. CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) - ## Add a device to the current active machine. - @staticmethod - def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None: - device.connect() - active_machine.addConfiguredConnectionType(device.connectionType.value) - CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) - ## Callback for when a manual device check request was responded to. def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus) -> None: callback = self._manual_instances.get(address, None) @@ -191,6 +187,7 @@ class NetworkOutputDeviceManager: active_machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) active_machine.setMetaDataEntry("group_name", device.name) self._connectToOutputDevice(device, active_machine) + CloudFlowMessage(device.ipAddress).show() # Nudge the user to start using Ultimaker Cloud. ## Load the user-configured manual devices from Cura preferences. def _getStoredManualInstances(self) -> Dict[str, Optional[Callable]]: @@ -199,8 +196,9 @@ class NetworkOutputDeviceManager: manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") return {address: None for address in manual_instances} - ## Handles an API error received from the cloud. - # \param errors: The errors received + ## Add a device to the current active machine. @staticmethod - def _onApiError(errors) -> None: - Logger.log("w", str(errors)) + def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None: + device.connect() + active_machine.addConfiguredConnectionType(device.connectionType.value) + CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 0b108e0a1b..416c60006b 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -53,142 +53,3 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): ## Remove a manually connected networked printer. def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: self._network_output_device_manager.removeManualDevice(key, address) - - # ## Check if the prerequsites are in place to start the cloud flow - # def checkCloudFlowIsPossible(self, cluster: Optional[CloudOutputDevice]) -> None: - # Logger.log("d", "Checking if cloud connection is possible...") - # - # # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again - # active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack] - # if active_machine: - # # Check 1A: Printer isn't already configured for cloud - # if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes: - # Logger.log("d", "Active machine was already configured for cloud.") - # return - # - # # Check 1B: Printer isn't already configured for cloud - # if active_machine.getMetaDataEntry("cloud_flow_complete", False): - # Logger.log("d", "Active machine was already configured for cloud.") - # return - # - # # Check 2: User did not already say "Don't ask me again" - # if active_machine.getMetaDataEntry("do_not_show_cloud_message", False): - # Logger.log("d", "Active machine shouldn't ask about cloud anymore.") - # return - # - # # Check 3: User is logged in with an Ultimaker account - # if not self._account.isLoggedIn: - # Logger.log("d", "Cloud Flow not possible: User not logged in!") - # return - # - # # Check 4: Machine is configured for network connectivity - # if not self._application.getMachineManager().activeMachineHasNetworkConnection: - # Logger.log("d", "Cloud Flow not possible: Machine is not connected!") - # return - # - # # Check 5: Machine has correct firmware version - # firmware_version = self._application.getMachineManager().activeMachineFirmwareVersion # type: str - # if not Version(firmware_version) > self._min_cloud_version: - # Logger.log("d", "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)", - # firmware_version, - # self._min_cloud_version) - # return - # - # Logger.log("d", "Cloud flow is possible!") - # self.cloudFlowIsPossible.emit() - - # def _onCloudFlowPossible(self) -> None: - # # Cloud flow is possible, so show the message - # if not self._start_cloud_flow_message: - # self._createCloudFlowStartMessage() - # if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible: - # self._start_cloud_flow_message.show() - - # def _onCloudPrintingConfigured(self, device) -> None: - # # Hide the cloud flow start message if it was hanging around already - # # For example: if the user already had the browser openen and made the association themselves - # if self._start_cloud_flow_message and self._start_cloud_flow_message.visible: - # self._start_cloud_flow_message.hide() - # - # # Cloud flow is complete, so show the message - # if not self._cloud_flow_complete_message: - # self._createCloudFlowCompleteMessage() - # if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible: - # self._cloud_flow_complete_message.show() - # - # # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers - # active_machine = self._application.getMachineManager().activeMachine - # if active_machine: - # - # # The active machine _might_ not be the machine that was in the added cloud cluster and - # # then this will hide the cloud message for the wrong machine. So we only set it if the - # # host names match between the active machine and the newly added cluster - # saved_host_name = active_machine.getMetaDataEntry("um_network_key", "").split('.')[0] - # added_host_name = device.toDict()["host_name"] - # - # if added_host_name == saved_host_name: - # active_machine.setMetaDataEntry("do_not_show_cloud_message", True) - # - # return - - # def _onDontAskMeAgain(self, checked: bool) -> None: - # active_machine = self._application.getMachineManager().activeMachine # type: Optional[GlobalStack] - # if active_machine: - # active_machine.setMetaDataEntry("do_not_show_cloud_message", checked) - # if checked: - # Logger.log("d", "Will not ask the user again to cloud connect for current printer.") - # return - - # def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None: - # address = self._application.getMachineManager().activeMachineAddress # type: str - # if address: - # QDesktopServices.openUrl(QUrl("http://" + address + "/cloud_connect")) - # if self._start_cloud_flow_message: - # self._start_cloud_flow_message.hide() - # self._start_cloud_flow_message = None - # return - - # def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None: - # address = self._application.getMachineManager().activeMachineAddress # type: str - # if address: - # QDesktopServices.openUrl(QUrl("http://" + address + "/settings")) - # return - - # def _onMachineSwitched(self) -> None: - # # Hide any left over messages - # if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible: - # self._start_cloud_flow_message.hide() - # if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible: - # self._cloud_flow_complete_message.hide() - # - # # Check for cloud flow again with newly selected machine - # self.checkCloudFlowIsPossible(None) - - # def _createCloudFlowStartMessage(self): - # self._start_cloud_flow_message = Message( - # text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), - # lifetime = 0, - # image_source = QUrl.fromLocalFile(os.path.join( - # PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - # "resources", "svg", "cloud-flow-start.svg" - # )), - # image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"), - # option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), - # option_state = False - # ) - # self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "") - # self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain) - # self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted) - - # def _createCloudFlowCompleteMessage(self): - # self._cloud_flow_complete_message = Message( - # text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), - # lifetime = 30, - # image_source = QUrl.fromLocalFile(os.path.join( - # PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), - # "resources", "svg", "cloud-flow-completed.svg" - # )), - # image_caption = i18n_catalog.i18nc("@info:status", "Connected!") - # ) - # self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon - # self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection) From dc826ab456d13ee1848bbae113faa88928d9a97b Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 30 Jul 2019 22:52:29 +0200 Subject: [PATCH 37/52] Fix typing --- plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 11f9ab033c..64b702472c 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -5,7 +5,7 @@ from json import JSONDecodeError from typing import Callable, List, Optional, Dict, Union, Any, Type, cast, TypeVar, Tuple from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkConfiguration +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from UM.Logger import Logger @@ -33,7 +33,6 @@ class ClusterApiClient: self._manager = QNetworkAccessManager() self._address = address self._on_error = on_error - self._upload = None # type: # Optional[ToolPathUploader] # In order to avoid garbage collection we keep the callbacks in this list. self._anti_gc_callbacks = [] # type: List[Callable[[], None]] @@ -129,7 +128,7 @@ class ClusterApiClient: reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], - model: Optional[Type[ClusterApiClientModel]] = None, + model: Type[ClusterApiClientModel] = None, ) -> None: def parse() -> None: From 60d47fcbad78c87050aa26808990aa655c869283 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 31 Jul 2019 09:20:33 +0200 Subject: [PATCH 38/52] Only remove output device if it actually existed --- .../src/Network/NetworkOutputDeviceManager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index ea62f2b205..7e67786712 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -22,7 +22,7 @@ I18N_CATALOG = i18nCatalog("cura") ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. class NetworkOutputDeviceManager: - + META_NETWORK_KEY = "um_network_key" MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" @@ -97,12 +97,13 @@ class NetworkOutputDeviceManager: if not active_machine: return + output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() stored_device_id = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) for device in self._discovered_devices.values(): if device.key == stored_device_id: # Connect to it if the stored key matches. self._connectToOutputDevice(device, active_machine) - else: + elif device.key in output_device_manager.getOutputDeviceIds(): # Remove device if it is not meant for the active machine. CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) From 7147c788efd8b9a585648340bd9574eb2b72c051 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 31 Jul 2019 12:27:33 +0200 Subject: [PATCH 39/52] Fix a bug that would pair a local and cloud printer when they were not the same --- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 2 +- .../UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 14c17c33a3..238f4d57df 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -128,7 +128,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): return True # However, for manually added printers, the local IP address is used in lieu of a proper # network key, so check for that as well - if self.clusterData.host_internal_ip is not None and network_key.find(self.clusterData.host_internal_ip): + if self.clusterData.host_internal_ip is not None and network_key in self.clusterData.host_internal_ip: return True return False diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index d3acc4037e..4c8058d91d 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -151,6 +151,7 @@ class CloudOutputDeviceManager: if not active_machine: return + output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) for device in self._remote_clusters.values(): @@ -161,9 +162,9 @@ class CloudOutputDeviceManager: # Connect to it if we can match the local network key that was already present. active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) self._connectToOutputDevice(device, active_machine) - else: + elif device.key in output_device_manager.getOutputDeviceIds(): # Remove device if it is not meant for the active machine. - CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) + output_device_manager.removeOutputDevice(device.key) ## Connects to an output device and makes sure it is registered in the output device manager. @staticmethod From 0ba8bf13e1b0641cb19d0ba6d4e6f741f611db4b Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:09:37 +0200 Subject: [PATCH 40/52] Fix copyright year to 2019 --- plugins/UM3NetworkPrinting/__init__.py | 2 +- plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml | 4 ++-- .../UM3NetworkPrinting/resources/qml/ExpandableCard.qml | 4 ++-- plugins/UM3NetworkPrinting/resources/qml/GenericPopUp.qml | 2 +- .../resources/qml/MonitorBuildplateConfiguration.qml | 8 ++++---- .../UM3NetworkPrinting/resources/qml/MonitorCarousel.qml | 8 ++++---- .../resources/qml/MonitorConfigOverrideDialog.qml | 2 +- .../resources/qml/MonitorContextMenu.qml | 2 +- .../resources/qml/MonitorContextMenuButton.qml | 4 ++-- .../resources/qml/MonitorExtruderConfiguration.qml | 8 ++++---- .../resources/qml/MonitorIconExtruder.qml | 4 ++-- .../UM3NetworkPrinting/resources/qml/MonitorInfoBlurb.qml | 2 +- plugins/UM3NetworkPrinting/resources/qml/MonitorItem.qml | 4 ++-- .../resources/qml/MonitorPrintJobCard.qml | 2 +- .../resources/qml/MonitorPrintJobPreview.qml | 2 +- .../resources/qml/MonitorPrintJobProgressBar.qml | 4 ++-- .../resources/qml/MonitorPrinterCard.qml | 2 +- .../resources/qml/MonitorPrinterConfiguration.qml | 4 ++-- .../resources/qml/MonitorPrinterPill.qml | 2 +- plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml | 2 +- plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml | 2 +- .../resources/qml/PrintJobContextMenuItem.qml | 4 ++-- plugins/UM3NetworkPrinting/resources/qml/PrintWindow.qml | 2 +- .../resources/qml/PrinterVideoStream.qml | 2 +- .../resources/qml/UM3InfoComponents.qml | 2 +- plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py | 8 ++++---- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 2 +- .../src/Cloud/CloudOutputDeviceManager.py | 2 +- plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py | 2 +- plugins/UM3NetworkPrinting/src/CloudFlowMessage.py | 4 ++-- plugins/UM3NetworkPrinting/src/ClusterOutputController.py | 2 +- plugins/UM3NetworkPrinting/src/MeshFormatHandler.py | 2 +- plugins/UM3NetworkPrinting/src/Models/BaseModel.py | 2 +- plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py | 2 +- .../src/Models/ConfigurationChangeModel.py | 2 +- .../src/Models/Http/CloudClusterResponse.py | 2 +- .../src/Models/Http/CloudClusterStatus.py | 2 +- plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py | 2 +- .../src/Models/Http/CloudPrintJobResponse.py | 2 +- .../src/Models/Http/CloudPrintJobUploadRequest.py | 2 +- .../src/Models/Http/CloudPrintResponse.py | 2 +- .../src/Models/Http/ClusterBuildPlate.py | 2 +- .../src/Models/Http/ClusterPrintCoreConfiguration.py | 2 +- .../src/Models/Http/ClusterPrintJobConfigurationChange.py | 2 +- .../src/Models/Http/ClusterPrintJobConstraint.py | 2 +- .../src/Models/Http/ClusterPrintJobImpediment.py | 2 +- .../src/Models/Http/ClusterPrintJobStatus.py | 2 +- .../Models/Http/ClusterPrinterConfigurationMaterial.py | 2 +- .../src/Models/Http/ClusterPrinterStatus.py | 2 +- .../src/Models/Http/PrinterSystemStatus.py | 2 +- plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py | 2 +- .../src/Models/UM3PrintJobOutputModel.py | 2 +- .../UM3NetworkPrinting/src/Network/ClusterApiClient.py | 8 ++++---- .../src/Network/NetworkOutputDeviceManager.py | 2 +- plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py | 2 +- .../src/PrintJobUploadProgressMessage.py | 2 +- plugins/UM3NetworkPrinting/src/Utils.py | 2 +- plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py | 4 ++-- plugins/UM3NetworkPrinting/tests/__init__.py | 2 +- 59 files changed, 84 insertions(+), 84 deletions(-) diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index fd083a7afa..88d96a71fe 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. # from .src import DiscoverUM3Action from .src import UM3OutputDevicePlugin diff --git a/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml b/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml index c0369cac0b..2e3d17ceb0 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 @@ -53,4 +53,4 @@ Rectangle } } } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml b/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml index fae8280488..5257361367 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -87,4 +87,4 @@ Item } } } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/GenericPopUp.qml b/plugins/UM3NetworkPrinting/resources/qml/GenericPopUp.qml index 74d9377f3e..61981dab2c 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/GenericPopUp.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/GenericPopUp.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml index 619658a7eb..5d08422877 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -35,7 +35,7 @@ Item { height: parent.height width: 32 * screenScaleFactor // Ensure the icon is centered under the extruder icon (same width) - + Rectangle { anchors.centerIn: parent @@ -56,7 +56,7 @@ Item visible: buildplate } } - + Label { id: buildplateLabel @@ -72,4 +72,4 @@ Item renderType: Text.NativeRendering } } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml index 0d7a177dd3..08743ed777 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 @@ -23,7 +23,7 @@ Item height: centerSection.height width: maximumWidth - + // Enable keyboard navigation Keys.onLeftPressed: navigateTo(currentIndex - 1) Keys.onRightPressed: navigateTo(currentIndex + 1) @@ -131,7 +131,7 @@ Item } } spacing: 60 * screenScaleFactor // TODO: Theme! - + Repeater { model: printers @@ -255,4 +255,4 @@ Item currentIndex = i } } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml index f0afb1fcaa..1fe766d9f7 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml index fbae66117e..34ca3c6df2 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenu.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenuButton.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenuButton.qml index e91e8b04d2..aa5d6de89b 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenuButton.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorContextMenuButton.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 @@ -29,4 +29,4 @@ Button hoverEnabled: enabled text: "\u22EE" //Unicode Three stacked points. width: 36 * screenScaleFactor // TODO: Theme! -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml index deed3ac5e6..63caaab433 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -56,7 +56,7 @@ Item Label { id: materialLabel - + color: UM.Theme.getColor("monitor_text_primary") elide: Text.ElideRight font: UM.Theme.getFont("default") // 12pt, regular @@ -86,7 +86,7 @@ Item Label { id: printCoreLabel - + color: UM.Theme.getColor("monitor_text_primary") elide: Text.ElideRight font: UM.Theme.getFont("default_bold") // 12pt, bold @@ -99,4 +99,4 @@ Item renderType: Text.NativeRendering } } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml index f6b84d69b2..876215d65d 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -50,4 +50,4 @@ Item visible: position >= 0 renderType: Text.NativeRendering } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorInfoBlurb.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorInfoBlurb.qml index 0d2c7f8beb..32e19c1cdb 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorInfoBlurb.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorInfoBlurb.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorItem.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorItem.qml index 41b3a93a7b..1ac72b8f8e 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorItem.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorItem.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -42,4 +42,4 @@ Component { z: 1; } } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml index 5aeedfd4ca..14e95559ec 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 import QtQuick.Controls 2.0 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml index d8749f18b6..7492b4e8e4 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml index e6d09b68f6..48bab48a9f 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 @@ -107,4 +107,4 @@ Item verticalAlignment: Text.AlignVCenter renderType: Text.NativeRendering } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml index 8562cec59d..e2b22312cd 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.3 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml index dbe085e18e..21d08a310c 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -55,4 +55,4 @@ Item anchors.bottom: parent.bottom buildplate: null } -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml index 3123631784..44aa1a1f8d 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml index cc9fad5233..b70759454a 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml index b92535a560..58e4263d2d 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrintJobContextMenuItem.qml b/plugins/UM3NetworkPrinting/resources/qml/PrintJobContextMenuItem.qml index ff5635e45d..78b94ce259 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/PrintJobContextMenuItem.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/PrintJobContextMenuItem.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 @@ -21,4 +21,4 @@ Button { height: visible ? 39 * screenScaleFactor : 0; // TODO: Theme! hoverEnabled: true; width: parent.width; -} \ No newline at end of file +} diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrintWindow.qml b/plugins/UM3NetworkPrinting/resources/qml/PrintWindow.qml index 548e5ce1ea..bcba60352c 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/PrintWindow.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/PrintWindow.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/resources/qml/PrinterVideoStream.qml index 77b481f6d8..cfbb30fdfb 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/PrinterVideoStream.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/PrinterVideoStream.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml index c99ed1688e..1da7c12e29 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2019 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 6b1b4725d0..cf1c2e6a8a 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import json from json import JSONDecodeError @@ -169,14 +169,14 @@ class CloudApiClient: Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], ) -> None: - + def parse() -> None: self._anti_gc_callbacks.remove(parse) - + # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: return - + status_code, response = self._parseReply(reply) self._parseModels(response, on_finished, model) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 238f4d57df..ef8bdb86dc 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from time import time from typing import List, Optional, cast diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 4c8058d91d..b31f1efa47 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Dict, List, Optional diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 55b41d1c1c..d5de7fe10a 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # !/usr/bin/env python # -*- coding: utf-8 -*- from PyQt5.QtCore import QUrl diff --git a/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py b/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py index ab299afad0..2d7b99b750 100644 --- a/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py +++ b/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os @@ -14,7 +14,7 @@ I18N_CATALOG = i18nCatalog("cura") class CloudFlowMessage(Message): - + def __init__(self, address: str) -> None: super().__init__( text=I18N_CATALOG.i18nc("@info:status", diff --git a/plugins/UM3NetworkPrinting/src/ClusterOutputController.py b/plugins/UM3NetworkPrinting/src/ClusterOutputController.py index 775297e2c0..02d8d174d1 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterOutputController.py +++ b/plugins/UM3NetworkPrinting/src/ClusterOutputController.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 1e76c22a50..1b13b6c91c 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import io from typing import Optional, Dict, Union, List, cast diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index 3fb83c330f..3d38a4b116 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime, timezone from typing import TypeVar, Dict, List, Any, Type, Union diff --git a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py index 31f5a39fac..a441f28292 100644 --- a/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/ClusterMaterial.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from .BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py b/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py index 7b81f6e431..58fae03679 100644 --- a/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/ConfigurationChangeModel.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtProperty, QObject diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index c2b87a9efb..7ecfe8b0a3 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index dbc5f24480..330e61d343 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime from typing import List, Dict, Union, Any diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py index 4ba8f50293..9381e4b8cf 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Dict, Optional, Any diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py index d88139acee..a1880e8751 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py index d221683a5b..ff705ae495 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from ..BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py index b9f5b24d86..b108f40e27 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime from typing import Optional, Union diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py index fdc425fceb..a5a392488d 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from ..BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index ffc80c1810..24c9a577f9 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Union, Dict, Optional, Any diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py index eebc73a70e..88251bbf53 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py index 97e1b63abd..9239004b18 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py index 63038fe3f6..5a8f0aa46d 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from ..BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index ea2adf4b1b..8b35fb7b5a 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import List, Optional, Union, Dict, Any diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index 68c83ba76b..378a885a3b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index c7aaea39bc..bd9d59b910 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import List, Union, Dict, Optional, Any diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py index ed85ed1799..01539bd365 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from ..BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py index f3a67686cb..b45289e1c4 100644 --- a/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/LocalMaterial.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from .BaseModel import BaseModel diff --git a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py index a9099a21aa..bfde233a35 100644 --- a/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/UM3PrintJobOutputModel.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import List diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 64b702472c..1025e384d6 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import json from json import JSONDecodeError @@ -130,14 +130,14 @@ class ClusterApiClient: Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None, ) -> None: - + def parse() -> None: self._anti_gc_callbacks.remove(parse) - + # Don't try to parse the reply if we didn't get one if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: return - + if reply.error() > 0: self._on_error(reply.errorString()) return diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py index 7e67786712..6efb41a59d 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from typing import Dict, Optional, Callable diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py index f70cadb495..d601aa3689 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from queue import Queue from threading import Thread, Event diff --git a/plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py b/plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py index 9862c9ec72..bdbab008e3 100644 --- a/plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py +++ b/plugins/UM3NetworkPrinting/src/PrintJobUploadProgressMessage.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM import i18nCatalog from UM.Message import Message diff --git a/plugins/UM3NetworkPrinting/src/Utils.py b/plugins/UM3NetworkPrinting/src/Utils.py index 8872576f8b..a628130416 100644 --- a/plugins/UM3NetworkPrinting/src/Utils.py +++ b/plugins/UM3NetworkPrinting/src/Utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime, timedelta diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 2cab110861..75be120ac5 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -1,5 +1,5 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import io import json diff --git a/plugins/UM3NetworkPrinting/tests/__init__.py b/plugins/UM3NetworkPrinting/tests/__init__.py index f3f6970c54..d5641e902f 100644 --- a/plugins/UM3NetworkPrinting/tests/__init__.py +++ b/plugins/UM3NetworkPrinting/tests/__init__.py @@ -1,2 +1,2 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. From 2628af1790ce294aff7e21d144cc5c513a3889e5 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:10:34 +0200 Subject: [PATCH 41/52] Remove commented import --- plugins/UM3NetworkPrinting/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index 88d96a71fe..bd916f06fc 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -1,6 +1,5 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -# from .src import DiscoverUM3Action from .src import UM3OutputDevicePlugin From 11439644c3393149dbf9288c0951cbd756db4209 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:11:43 +0200 Subject: [PATCH 42/52] Fix dangling close bracket --- plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index cf1c2e6a8a..3b6aff31a5 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -70,8 +70,8 @@ class CloudApiClient: ## Requests the cloud to register the upload of a print job mesh. # \param request: The request object. # \param on_finished: The function to be called after the result is parsed. - def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any] - ) -> None: + def requestUpload(self, request: CloudPrintJobUploadRequest, + on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: url = "{}/jobs/upload".format(self.CURA_API_ROOT) body = json.dumps({"data": request.toDict()}) reply = self._manager.put(self._createEmptyRequest(url), body.encode()) @@ -167,9 +167,7 @@ class CloudApiClient: reply: QNetworkReply, on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], - model: Type[CloudApiClientModel], - ) -> None: - + model: Type[CloudApiClientModel]) -> None: def parse() -> None: self._anti_gc_callbacks.remove(parse) From baa7b485ef8c1598e3e86ef83a5891269f89b3dc Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:12:16 +0200 Subject: [PATCH 43/52] Fix dangling close bracket --- plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 3b6aff31a5..06cabdc463 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -101,8 +101,8 @@ class CloudApiClient: # \param cluster_id: The ID of the cluster. # \param cluster_job_id: The ID of the print job within the cluster. # \param action: The name of the action to execute. - def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None - ) -> None: + def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, + data: Optional[Dict[str, Any]] = None) -> None: body = json.dumps({"data": data}).encode() if data else b"" url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) self._manager.post(self._createEmptyRequest(url), body) From 1fa5628cb2e35425d1b42f9dbb20a9bda45da032 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:16:08 +0200 Subject: [PATCH 44/52] Rename output device to prevent inheritance naming confusion --- cura/Machines/Models/DiscoveredPrintersModel.py | 2 +- plugins/ThingiBrowser | 1 + ...rkOutputDevice.py => LocalClusterOutputDevice.py} | 2 +- ...Manager.py => LocalClusterOutputDeviceManager.py} | 12 ++++++------ plugins/UM3NetworkPrinting/src/SendMaterialJob.py | 6 +++--- .../UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) create mode 120000 plugins/ThingiBrowser rename plugins/UM3NetworkPrinting/src/Network/{NetworkOutputDevice.py => LocalClusterOutputDevice.py} (99%) rename plugins/UM3NetworkPrinting/src/Network/{NetworkOutputDeviceManager.py => LocalClusterOutputDeviceManager.py} (95%) diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index a1b68ee1ae..33e0b7a4d9 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject): def readableMachineType(self) -> str: from cura.CuraApplication import CuraApplication machine_manager = CuraApplication.getInstance().getMachineManager() - # In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field + # In LocalClusterOutputDevice, when it updates a printer information, it updates the machine type using the field # "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string # like "Ultimaker 3". The code below handles this case. if self._hasHumanReadableMachineTypeName(self._machine_type): diff --git a/plugins/ThingiBrowser b/plugins/ThingiBrowser new file mode 120000 index 0000000000..cd517534c1 --- /dev/null +++ b/plugins/ThingiBrowser @@ -0,0 +1 @@ +/home/cterbeke/Code/ChrisTerBeke/ThingiBrowser \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py similarity index 99% rename from plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py rename to plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 7f6b2f54b3..f8cf652114 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -23,7 +23,7 @@ from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOut I18N_CATALOG = i18nCatalog("cura") -class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice): +class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): activeCameraUrlChanged = pyqtSignal() diff --git a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py similarity index 95% rename from plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py rename to plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 6efb41a59d..6f48fa26a4 100644 --- a/plugins/UM3NetworkPrinting/src/Network/NetworkOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -12,7 +12,7 @@ from cura.Settings.GlobalStack import GlobalStack from .ZeroConfClient import ZeroConfClient from .ClusterApiClient import ClusterApiClient -from .NetworkOutputDevice import NetworkOutputDevice +from .LocalClusterOutputDevice import LocalClusterOutputDevice from ..CloudFlowMessage import CloudFlowMessage from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus @@ -20,8 +20,8 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus I18N_CATALOG = i18nCatalog("cura") -## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. -class NetworkOutputDeviceManager: +## The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters. +class LocalClusterOutputDeviceManager: META_NETWORK_KEY = "um_network_key" @@ -37,7 +37,7 @@ class NetworkOutputDeviceManager: def __init__(self) -> None: # Persistent dict containing the networked clusters. - self._discovered_devices = {} # type: Dict[str, NetworkOutputDevice] + self._discovered_devices = {} # type: Dict[str, LocalClusterOutputDevice] self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() # Hook up ZeroConf client. @@ -152,7 +152,7 @@ class NetworkOutputDeviceManager: if cluster_size == -1: return - device = NetworkOutputDevice(key, address, properties) + device = LocalClusterOutputDevice(key, address, properties) CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( ip_address=address, key=device.getId(), @@ -167,7 +167,7 @@ class NetworkOutputDeviceManager: ## Remove a device. def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: - device = self._discovered_devices.pop(device_id, None) # type: Optional[NetworkOutputDevice] + device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice] if not device: return device.close() diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 50705efa8e..697ba33a6b 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -13,7 +13,7 @@ from .Models.ClusterMaterial import ClusterMaterial from .Models.LocalMaterial import LocalMaterial if TYPE_CHECKING: - from .Network.NetworkOutputDevice import NetworkOutputDevice + from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice ## Asynchronous job to send material profiles to the printer. @@ -21,9 +21,9 @@ if TYPE_CHECKING: # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): - def __init__(self, device: "NetworkOutputDevice") -> None: + def __init__(self, device: "LocalClusterOutputDevice") -> None: super().__init__() - self.device = device # type: NetworkOutputDevice + self.device = device # type: LocalClusterOutputDevice ## Send the request to the printer and register a callback def run(self) -> None: diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 416c60006b..64180491fe 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -7,7 +7,7 @@ from cura.CuraApplication import CuraApplication from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin -from .Network.NetworkOutputDeviceManager import NetworkOutputDeviceManager +from .Network.LocalClusterOutputDeviceManager import LocalClusterOutputDeviceManager from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager @@ -18,7 +18,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): super().__init__() # Create a network output device manager that abstracts all network connection logic away. - self._network_output_device_manager = NetworkOutputDeviceManager() + self._network_output_device_manager = LocalClusterOutputDeviceManager() # Create a cloud output device manager that abstracts all cloud connection logic away. self._cloud_output_device_manager = CloudOutputDeviceManager() From e7fdd02b6f804e48cc3ce52b2c7c792c32c336d7 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:26:24 +0200 Subject: [PATCH 45/52] First round of CI codestyle fixes --- plugins/UM3NetworkPrinting/src/CloudFlowMessage.py | 11 +++++++---- plugins/UM3NetworkPrinting/src/ExportFileJob.py | 4 ++-- plugins/UM3NetworkPrinting/src/MeshFormatHandler.py | 2 +- .../src/Network/LocalClusterOutputDevice.py | 3 +-- .../src/UltimakerNetworkedPrinterOutputDevice.py | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py b/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py index 2d7b99b750..4f2f7a71a2 100644 --- a/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py +++ b/plugins/UM3NetworkPrinting/src/CloudFlowMessage.py @@ -16,16 +16,19 @@ I18N_CATALOG = i18nCatalog("cura") class CloudFlowMessage(Message): def __init__(self, address: str) -> None: + + image_path = os.path.join( + CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "", + "resources", "svg", "cloud-flow-start.svg" + ) + super().__init__( text=I18N_CATALOG.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), lifetime=0, dismissable=True, option_state=False, - image_source=QUrl.fromLocalFile(os.path.join( - CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting"), - "resources", "svg", "cloud-flow-start.svg" - )), + image_source=image_path, image_caption=I18N_CATALOG.i18nc("@info:status Ultimaker Cloud should not be translated.", "Connect to Ultimaker Cloud"), ) diff --git a/plugins/UM3NetworkPrinting/src/ExportFileJob.py b/plugins/UM3NetworkPrinting/src/ExportFileJob.py index 7abe1e6d8e..56d15bc835 100644 --- a/plugins/UM3NetworkPrinting/src/ExportFileJob.py +++ b/plugins/UM3NetworkPrinting/src/ExportFileJob.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.WriteFileJob import WriteFileJob @@ -12,7 +12,7 @@ from .MeshFormatHandler import MeshFormatHandler ## Job that exports the build plate to the correct file format for the target cluster. class ExportFileJob(WriteFileJob): - def __init__(self, file_handler: FileHandler, nodes: List[SceneNode], firmware_version: str) -> None: + def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str) -> None: self._mesh_format_handler = MeshFormatHandler(file_handler, firmware_version) if not self._mesh_format_handler.is_valid: diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 1b13b6c91c..9927bf744e 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -32,7 +32,7 @@ class MeshFormatHandler: # \return A dict with the file format details, with the following keys: # {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool} @property - def preferred_format(self) -> Optional[Dict[str, Union[str, int, bool]]]: + def preferred_format(self) -> Dict[str, Union[str, int, bool]]: return self._preferred_format ## Gets the file writer for the given file handler and mime type. diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index f8cf652114..758760ce86 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -7,7 +7,6 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty from PyQt5.QtNetwork import QNetworkReply from UM.FileHandler.FileHandler import FileHandler -from UM.FileHandler.WriteFileJob import WriteFileJob from UM.Message import Message from UM.i18n import i18nCatalog from UM.Scene.SceneNode import SceneNode @@ -130,7 +129,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Handler for when the print job was created locally. # It can now be sent over the network. - def _onPrintJobCreated(self, job: WriteFileJob) -> None: + def _onPrintJobCreated(self, job: ExportFileJob) -> None: self._progress.show() parts = [ self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"), diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index fedead7bce..9e21c0b4de 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -191,7 +191,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Keep track of the new printers to show. # We create a new list instead of changing the existing one to get the correct order. - new_printers = [] + new_printers = [] # type: List[PrinterOutputModel] # Check which printers need to be created or updated. for index, printer_data in enumerate(remote_printers): From bd4c4b1f1d1d2b25566d29df91322bc49416f98d Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:37:11 +0200 Subject: [PATCH 46/52] 2nd round of CI codestyle fixes --- .../UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 10 ++++++---- .../src/Network/LocalClusterOutputDeviceManager.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index ef8bdb86dc..f273f537e3 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -9,7 +9,6 @@ from PyQt5.QtGui import QDesktopServices from UM import i18nCatalog from UM.Backend.Backend import BackendState from UM.FileHandler.FileHandler import FileHandler -from UM.FileHandler.WriteFileJob import WriteFileJob from UM.Logger import Logger from UM.Message import Message from UM.Scene.SceneNode import SceneNode @@ -191,11 +190,12 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Handler for when the print job was created locally. # It can now be sent over the cloud. - def _onPrintJobCreated(self, job: WriteFileJob) -> None: - self._tool_path = job.getOutput() + def _onPrintJobCreated(self, job: ExportFileJob) -> None: + output = job.getOutput() + self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again request = CloudPrintJobUploadRequest( job_name=job.getFileName(), - file_size=len(self._tool_path), + file_size=len(output), content_type=job.getMimeType(), ) self._api.requestUpload(request, self._uploadPrintJob) @@ -203,6 +203,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Uploads the mesh when the print job was registered with the cloud API. # \param job_response: The response received from the cloud API. def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None: + if not self._tool_path: + return self._onUploadError() self._progress.show() self._uploaded_print_job = job_response # store the last uploaded job to prevent re-upload of the same file self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update, diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 6f48fa26a4..47a7df7faf 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -46,7 +46,7 @@ class LocalClusterOutputDeviceManager: self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved) # Persistent dict containing manually connected clusters. - self._manual_instances = {} # type: Dict[str, Callable] + self._manual_instances = {} # type: Dict[str, Optional[Callable]] ## Start the network discovery. def start(self) -> None: From 91ac7dbc24ca22a9df980a50a0a29fcd151e626f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:46:14 +0200 Subject: [PATCH 47/52] Some defenses against possible None values --- plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py | 5 +++++ .../src/UltimakerNetworkedPrinterOutputDevice.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py index d601aa3689..b6416b2bd0 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -53,11 +53,16 @@ class ZeroConfClient: ## Handles a change is discovered network services. def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None: item = (zeroconf, service_type, name, state_change) + if not self._service_changed_request_queue or not self._service_changed_request_event: + return self._service_changed_request_queue.put(item) self._service_changed_request_event.set() ## Callback for when a ZeroConf service has changes. def _handleOnServiceChangedRequests(self) -> None: + if not self._service_changed_request_queue or not self._service_changed_request_event: + return + while True: # Wait for the event to be set self._service_changed_request_event.wait(timeout=5.0) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 9e21c0b4de..870ba3c47c 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -195,7 +195,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Check which printers need to be created or updated. for index, printer_data in enumerate(remote_printers): - printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid), None) + printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid)) if not printer: printer = printer_data.createOutputModel(ClusterOutputController(self)) else: From a2345d300451f761e39f731dc55aa899488e679d Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:54:52 +0200 Subject: [PATCH 48/52] Fix --- .../src/UltimakerNetworkedPrinterOutputDevice.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 870ba3c47c..1bfbd50e9d 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -195,12 +195,13 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Check which printers need to be created or updated. for index, printer_data in enumerate(remote_printers): - printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid)) - if not printer: - printer = printer_data.createOutputModel(ClusterOutputController(self)) - else: + printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid), None) + if printer: printer_data.updateOutputModel(printer) - new_printers.append(printer) + new_printers.append(printer) + else: + printer = printer_data.createOutputModel(ClusterOutputController(self)) + new_printers.append(printer) # Check which printers need to be removed (de-referenced). remote_printers_keys = [printer_data.uuid for printer_data in remote_printers] From 66dbbf7fb672e370dbdbb0f143340a64c4d98bd5 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:59:55 +0200 Subject: [PATCH 49/52] Use correct none checking (hopefully) --- .../src/UltimakerNetworkedPrinterOutputDevice.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 1bfbd50e9d..70e85879cf 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -196,12 +196,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Check which printers need to be created or updated. for index, printer_data in enumerate(remote_printers): printer = next(iter(printer for printer in self._printers if printer.key == printer_data.uuid), None) - if printer: - printer_data.updateOutputModel(printer) - new_printers.append(printer) - else: + if printer is None: printer = printer_data.createOutputModel(ClusterOutputController(self)) - new_printers.append(printer) + else: + printer_data.updateOutputModel(printer) + new_printers.append(printer) # Check which printers need to be removed (de-referenced). remote_printers_keys = [printer_data.uuid for printer_data in remote_printers] From 22aff6468f3cf158194b7765a877a112ebd3b96e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sun, 4 Aug 2019 22:18:02 +0200 Subject: [PATCH 50/52] Delete ThingiBrowser --- plugins/ThingiBrowser | 1 - 1 file changed, 1 deletion(-) delete mode 120000 plugins/ThingiBrowser diff --git a/plugins/ThingiBrowser b/plugins/ThingiBrowser deleted file mode 120000 index cd517534c1..0000000000 --- a/plugins/ThingiBrowser +++ /dev/null @@ -1 +0,0 @@ -/home/cterbeke/Code/ChrisTerBeke/ThingiBrowser \ No newline at end of file From d19e13daa6717f6bc0fdd716d69a50407c0434ba Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sun, 4 Aug 2019 22:20:03 +0200 Subject: [PATCH 51/52] Delete __init__.py --- plugins/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 plugins/__init__.py diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 8557115c647770d5487e4af75c92c45dd3027965 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 5 Aug 2019 09:29:44 +0200 Subject: [PATCH 52/52] Add plugins/__init__.py to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2ec5af2b9b..a66c1086a7 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ run.sh CuraEngine /.coverage + +#Prevents import failures when plugin running tests +plugins/__init__.py