From 3c1b3773085ed64f22e623a8dff3503cd8ef088c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 26 Jul 2019 15:07:52 +0200 Subject: [PATCH 01/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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 b517a031c004fe2935cf67522b30af9003d52b8f Mon Sep 17 00:00:00 2001 From: PM Date: Wed, 31 Jul 2019 16:12:29 +0200 Subject: [PATCH 40/60] Added FelixPro2 printer definition files --- resources/definitions/felixpro2dual.def.json | 74 + .../felixpro2_dual_extruder_0.def.json | 28 + .../felixpro2_dual_extruder_1.def.json | 28 + resources/images/FelixPro2_platform.png | Bin 0 -> 4481 bytes resources/meshes/FelixPro2_platform.obj | 4485 +++++++++++++++++ resources/variants/felixpro2_0.25.inst.cfg | 14 + resources/variants/felixpro2_0.35.inst.cfg | 14 + resources/variants/felixpro2_0.50.inst.cfg | 13 + resources/variants/felixpro2_0.70.inst.cfg | 14 + 9 files changed, 4670 insertions(+) create mode 100644 resources/definitions/felixpro2dual.def.json create mode 100644 resources/extruders/felixpro2_dual_extruder_0.def.json create mode 100644 resources/extruders/felixpro2_dual_extruder_1.def.json create mode 100644 resources/images/FelixPro2_platform.png create mode 100644 resources/meshes/FelixPro2_platform.obj create mode 100644 resources/variants/felixpro2_0.25.inst.cfg create mode 100644 resources/variants/felixpro2_0.35.inst.cfg create mode 100644 resources/variants/felixpro2_0.50.inst.cfg create mode 100644 resources/variants/felixpro2_0.70.inst.cfg diff --git a/resources/definitions/felixpro2dual.def.json b/resources/definitions/felixpro2dual.def.json new file mode 100644 index 0000000000..39d0db55a8 --- /dev/null +++ b/resources/definitions/felixpro2dual.def.json @@ -0,0 +1,74 @@ +{ + "version": 2, + "name": "Felix Pro 2 Dual", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "pnks", + "manufacturer": "Felix", + "platform": "FelixPro2_platform.obj", + "platform_texture": "FelixPro2_platform.png", + "platform_offset": [-135,0,130], + "machine_extruder_trains": + { + "0": "felixpro2_dual_extruder_0", + "1": "felixpro2_dual_extruder_1" + }, + "file_formats": "text/x-gcode", + "has_variants": true, + "has_materials": true, + "preferred_variant_name": "0.35 mm", + "variants_name": "Nozzle diameter" + }, + "overrides": { + "machine_name": { "default_value": "FelixPro2Dual" }, + + "layer_height": { "default_value": 0.15 }, + "layer_height_0": { "default_value": 0.2 }, + "speed_layer_0": { "default_value": 20}, + + "infill_sparse_density": { "default_value": 20 }, + "wall_thickness": { "default_value": 1 }, + "top_bottom_thickness": { "default_value": 1 }, + + "machine_width": { "default_value": 240 }, + "machine_depth": { "default_value": 225 }, + "machine_height": { "default_value": 245 }, + + "machine_head_with_fans_polygon": + { + "default_value": [ + [ -60, 50 ], + [ -60, -50 ], + [ 70, 50 ], + [ 70, -50 ] + ] + }, + "gantry_height": { "value": "0" }, + "machine_extruder_count": { "default_value": 2 }, + "prime_tower_position_x": { "value": "250" }, + "prime_tower_position_y": { "value": "200" }, + + "machine_heated_bed": { "default_value": true }, + "machine_gcode_flavor": { "default_value": "Repetier" }, + "machine_center_is_zero": { "default_value": false }, + + "speed_print": { "default_value": 80 }, + "speed_travel": { "default_value": 200 }, + + "retraction_amount": { "default_value": 1 }, + "retraction_speed": { "default_value": 50}, + "material_flow": { "default_value": 100 }, + "material_flow_layer_0": { "default_value" : 110, "value": "material_flow * 1.1"}, + "adhesion_type": { "default_value": "skirt" }, + "skirt_brim_minimal_length": { "default_value": 130}, + "skirt_line_count": { "default_value": 3 }, + + "machine_start_gcode": { + "default_value": "G90 ;absolute positioning\r\nM82 ;set extruder to absolute mode\r\nM107 ;start with the fan off\r\nG28 X0 Y0 ;move X\/Y to min endstops\r\nG28 Z0 ;move Z to min endstops\r\nG1 Z15.0 F9000 ;move the platform down 15mm\r\n\r\nT0 ;Switch to the 1st extruder\r\nG92 E0 ;zero the extruded length\r\nG1 F200 E6 ;extrude 6 mm of feed stock\r\nG92 E0 ;zero the extruded length again\r\n;G1 F9000\r\nM117 FPro2 printing...\r\n" + }, + "machine_end_gcode": { + "default_value": "; Endcode FELIXprinters Pro series\r\n; =================================\t; Move extruder to park position\r\nG91 \t\t\t\t\t; Make coordinates relative\r\nG1 Z2 F5000 \t\t\t\t; Move z 2mm up\r\nG90 \t\t\t\t\t; Use absolute coordinates again\t\t\r\nG1 X220 Y243 F7800 \t\t\t; Move bed and printhead to ergonomic position\r\n\r\n; =================================\t; Turn off heaters\r\nT0\t\t\t\t\t; Select left extruder\r\nM104 T0 S0\t\t\t\t; Turn off heater and continue\t\t\t\t\r\nG92 E0\t\t\t\t\t; Reset extruder position\r\nG1 E-8\t\t\t\t\t; Retract filament 8mm\r\nG1 E-5\t\t\t\t\t; Push back filament 3mm\r\nG92 E0\t\t\t\t\t; Reset extruder position\r\n\r\nT1\t\t\t\t\t; Select right extruder\r\nM104 T1 S0\t\t\t\t; Turn off heater and continu\r\nG92 E0\t\t\t\t\t; Reset extruder position\r\nG1 E-8\t\t\t\t\t; Retract filament 8mm\r\nG1 E-5\t\t\t\t\t; Push back filament 3mm\r\nG92 E0\t\t\t\t\t; Reset extruder position\r\nT0\t\t\t\t\t; Select left extruder\r\nM140 S0\t\t\t\t\t; Turn off bed heater\r\n\r\n; =================================\t; Turn the rest off\r\nM107 \t\t\t\t; Turn off fan\r\nM84\t\t\t\t\t; Disable steppers\r\nM117 Print Complete" + } + } +} diff --git a/resources/extruders/felixpro2_dual_extruder_0.def.json b/resources/extruders/felixpro2_dual_extruder_0.def.json new file mode 100644 index 0000000000..90c41a83b5 --- /dev/null +++ b/resources/extruders/felixpro2_dual_extruder_0.def.json @@ -0,0 +1,28 @@ +{ + "id": "felixpro2_dual_extruder_0", + "version": 2, + "name": "Left Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "felixpro2dual", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.35 }, + "material_diameter": { "default_value": 1.75 }, + + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_start_pos_y": { "value": "prime_tower_position_y" }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_end_pos_y": { "value": "prime_tower_position_y" } + } +} diff --git a/resources/extruders/felixpro2_dual_extruder_1.def.json b/resources/extruders/felixpro2_dual_extruder_1.def.json new file mode 100644 index 0000000000..3ff0d401fd --- /dev/null +++ b/resources/extruders/felixpro2_dual_extruder_1.def.json @@ -0,0 +1,28 @@ +{ + "id": "felixpro2_dual_extruder_1", + "version": 2, + "name": "Right Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "felixpro2dual", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "2" + }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.35 }, + "material_diameter": { "default_value": 1.75 }, + + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_start_pos_y": { "value": "prime_tower_position_y" }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_end_pos_y": { "value": "prime_tower_position_y" } + } +} diff --git a/resources/images/FelixPro2_platform.png b/resources/images/FelixPro2_platform.png new file mode 100644 index 0000000000000000000000000000000000000000..be88994a40065d65a6b0bf9679ba447dea52b204 GIT binary patch literal 4481 zcmeAS@N?(olHy`uVBq!ia0y~yU;#3j7&zE~l+0PVoj?K3s*s41pu}>8f};Gi%$!t( zlFEWqh1817GzNx>TWiCk%Ve~9{#@63Cp6(mv8B!VlYE>0L|^z)k(N}RCS%T*5T)x9 z>Urc!{eP)1@^XfcPVN$@{dDia#*=C%XSfvGRDR8Q^XmNZcURj>57|c=ADonQ<+1CR z-<$hNBR0JGTGcaWzzK9iF>3&ANJ8 zaOoqCStiLmFG82!%_(*b*KapwO66euyk(=iGwYYlwqN`^<$NYsam7)?~7`%HWe5;J1 zW5ZTwgEa{UD#DJ-39?Q+z@wndaf&1MiWejEn>i*dnqgO)Wu6#(J7(TEeWiuvv_PJ# zldQgaoUjNMPKkWIV19zK$&|TPujhQ$+wo%3>A1gV4Bj`-4>e`yHQVvPnPZ|8mu1$L zi>~s{J%&f;sA#TQ=Y8V1NU)b>@@1a$lJV&hPhys2Y&KPc1+>T$(fxwYki)nrK@pnI>gyjGI`Fi$%Y0Q z*4@%IkMoS(9X`5OEEW*Y|Fr(h#U<}tpZ$CC*7(}3J@Z~Cz418AxhgQ_{)H*3kJz_A zyRIxd`C-gs1FIFEtlCzuQQkRW4!7m^h+{LRWI2{ScJp2QEF;BaTVC9X7cz;58iJN* z8hvPgaOl&^!mPZYuH3SHZxgIqEQ_!9G9KG?B|-7bqIH4S7v5STb^i&+)+;L}zu7PU zyiUcJooQ*fanRr2<^CFHeyH(%VK~yp_-sD&$Hv2MjQ>~Ou(-f1pw7e1lJJe=uqxLV zHlFDk7q|`fsIdLwYAHYJkbj_{vZ{)?&0FL}An$!~X4_Q{er0tmjUwAF zE#CJXjqAJ)TZ4r!dxS52C~J5jG_%5;=ZiCI&`WvlC%nv6f9LHGW8PcmAJg4fx5B{< zjla&Qh@H9PK>VaH_J@`|y|CN4e;oq@V{4|fbAYF_GptYm76o%^C)#=(c91z5AG}ma zR8YG^BV~o3n5(PI(k|Bx0k^o;TFr?0rD&4r5fmfCdcCPxzB`m%JUv!6hSl){^O2)h zJukX?{fQ6C5PW>F(Ei@t>N|UxTcSEwrrow!kTq>$kLyvNGFP5GN)3u%5_xQejK5Z7 zDe|8GSjsOS|6a-F^zS?K7ki(a$@8J}$YvjNokyjQUix}J-{UWn!MP|ku_QG`p**uBL&4qC zHz2%`Pn>~)+u75_F{I+w+Z(qz4>>Tn9^8HN$AOY9Uvrwn62vndSgbo-TvrP;otaW# z=byQ@mg#`nmwT=c-rvc;er9^{vX4_&+7(M(mw7$w@P`xo^lna7UAm{0sX<;^1!z43 zGl(z%5d@hBz^ZsaBta%|CPJ(v&O{_DX=OMGVL`gfNiZDAt3)zsWH>1S4OUB}bHOoA zis7S4gaHH)(M(D}L)0SlA{m37Ns8g5#NucY0mdO}EKUFkkdnGbqY)Hh2S5ZQ+(=1) zGz#d^$^dJsM2aMWOj^YvO=}vgzQ$gpjwTVTfkiM~ASs|Ei6rda_w Date: Wed, 31 Jul 2019 18:45:33 +0200 Subject: [PATCH 41/60] Creality machines - lower Z-hop speed to 5 --- resources/definitions/creality_base.def.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/definitions/creality_base.def.json b/resources/definitions/creality_base.def.json index 47f941e998..48311e0a5a 100644 --- a/resources/definitions/creality_base.def.json +++ b/resources/definitions/creality_base.def.json @@ -52,6 +52,7 @@ "speed_prime_tower": { "value": "speed_topbottom" }, "speed_support": { "value": "speed_wall_0" }, "speed_support_interface": { "value": "speed_topbottom" }, + "speed_z_hop": {"value": 5}, "skirt_brim_speed": { "value": "speed_layer_0" }, From 0ba8bf13e1b0641cb19d0ba6d4e6f741f611db4b Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 2 Aug 2019 15:09:37 +0200 Subject: [PATCH 42/60] 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 43/60] 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 44/60] 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 45/60] 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 46/60] 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 47/60] 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 48/60] 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 49/60] 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 50/60] 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 51/60] 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 52/60] 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 53/60] 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 54/60] 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 From 1ea1914d5a741f8272d6d44c22ee1dcc8a865f93 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 5 Aug 2019 10:24:05 +0200 Subject: [PATCH 55/60] Fix whitespace and code style Contributes to issue CURA-6701. --- resources/definitions/felixpro2dual.def.json | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/resources/definitions/felixpro2dual.def.json b/resources/definitions/felixpro2dual.def.json index 39d0db55a8..37c576952e 100644 --- a/resources/definitions/felixpro2dual.def.json +++ b/resources/definitions/felixpro2dual.def.json @@ -6,9 +6,9 @@ "visible": true, "author": "pnks", "manufacturer": "Felix", - "platform": "FelixPro2_platform.obj", - "platform_texture": "FelixPro2_platform.png", - "platform_offset": [-135,0,130], + "platform": "FelixPro2_platform.obj", + "platform_texture": "FelixPro2_platform.png", + "platform_offset": [-135, 0, 130], "machine_extruder_trains": { "0": "felixpro2_dual_extruder_0", @@ -25,7 +25,7 @@ "layer_height": { "default_value": 0.15 }, "layer_height_0": { "default_value": 0.2 }, - "speed_layer_0": { "default_value": 20}, + "speed_layer_0": { "default_value": 20}, "infill_sparse_density": { "default_value": 20 }, "wall_thickness": { "default_value": 1 }, @@ -34,8 +34,8 @@ "machine_width": { "default_value": 240 }, "machine_depth": { "default_value": 225 }, "machine_height": { "default_value": 245 }, - - "machine_head_with_fans_polygon": + + "machine_head_with_fans_polygon": { "default_value": [ [ -60, 50 ], @@ -45,10 +45,10 @@ ] }, "gantry_height": { "value": "0" }, - "machine_extruder_count": { "default_value": 2 }, + "machine_extruder_count": { "default_value": 2 }, "prime_tower_position_x": { "value": "250" }, - "prime_tower_position_y": { "value": "200" }, - + "prime_tower_position_y": { "value": "200" }, + "machine_heated_bed": { "default_value": true }, "machine_gcode_flavor": { "default_value": "Repetier" }, "machine_center_is_zero": { "default_value": false }, @@ -59,10 +59,10 @@ "retraction_amount": { "default_value": 1 }, "retraction_speed": { "default_value": 50}, "material_flow": { "default_value": 100 }, - "material_flow_layer_0": { "default_value" : 110, "value": "material_flow * 1.1"}, + "material_flow_layer_0": { "default_value" : 110, "value": "material_flow * 1.1" }, "adhesion_type": { "default_value": "skirt" }, - "skirt_brim_minimal_length": { "default_value": 130}, - "skirt_line_count": { "default_value": 3 }, + "skirt_brim_minimal_length": { "default_value": 130 }, + "skirt_line_count": { "default_value": 3 }, "machine_start_gcode": { "default_value": "G90 ;absolute positioning\r\nM82 ;set extruder to absolute mode\r\nM107 ;start with the fan off\r\nG28 X0 Y0 ;move X\/Y to min endstops\r\nG28 Z0 ;move Z to min endstops\r\nG1 Z15.0 F9000 ;move the platform down 15mm\r\n\r\nT0 ;Switch to the 1st extruder\r\nG92 E0 ;zero the extruded length\r\nG1 F200 E6 ;extrude 6 mm of feed stock\r\nG92 E0 ;zero the extruded length again\r\n;G1 F9000\r\nM117 FPro2 printing...\r\n" From 3847060b73c9c7f54e0998aa86a531eb324d56e9 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 5 Aug 2019 10:29:00 +0200 Subject: [PATCH 56/60] Upgrade to setting_version 9 Contributes to issue CURA-6701. --- resources/variants/felixpro2_0.25.inst.cfg | 2 +- resources/variants/felixpro2_0.35.inst.cfg | 2 +- resources/variants/felixpro2_0.50.inst.cfg | 2 +- resources/variants/felixpro2_0.70.inst.cfg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/variants/felixpro2_0.25.inst.cfg b/resources/variants/felixpro2_0.25.inst.cfg index 67abad7d3c..9eb89502b6 100644 --- a/resources/variants/felixpro2_0.25.inst.cfg +++ b/resources/variants/felixpro2_0.25.inst.cfg @@ -6,7 +6,7 @@ definition = felixpro2dual [metadata] author = pnks type = variant -setting_version = 7 +setting_version = 9 hardware_type = nozzle [values] diff --git a/resources/variants/felixpro2_0.35.inst.cfg b/resources/variants/felixpro2_0.35.inst.cfg index e8ef7376c5..a4d0848f63 100644 --- a/resources/variants/felixpro2_0.35.inst.cfg +++ b/resources/variants/felixpro2_0.35.inst.cfg @@ -6,7 +6,7 @@ definition = felixpro2dual [metadata] author = pnks type = variant -setting_version = 7 +setting_version = 9 hardware_type = nozzle [values] diff --git a/resources/variants/felixpro2_0.50.inst.cfg b/resources/variants/felixpro2_0.50.inst.cfg index 356518259c..2c7ff3bb6c 100644 --- a/resources/variants/felixpro2_0.50.inst.cfg +++ b/resources/variants/felixpro2_0.50.inst.cfg @@ -6,7 +6,7 @@ definition = felixpro2dual [metadata] author = pnks type = variant -setting_version = 7 +setting_version = 9 hardware_type = nozzle [values] diff --git a/resources/variants/felixpro2_0.70.inst.cfg b/resources/variants/felixpro2_0.70.inst.cfg index 6b659b6b18..b5de103f9d 100644 --- a/resources/variants/felixpro2_0.70.inst.cfg +++ b/resources/variants/felixpro2_0.70.inst.cfg @@ -6,7 +6,7 @@ definition = felixpro2dual [metadata] author = pnks type = variant -setting_version = 7 +setting_version = 9 hardware_type = nozzle [values] From f3bb922c0d90eaf4f6fafde0036bdbe559cf8a49 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 5 Aug 2019 10:33:39 +0200 Subject: [PATCH 57/60] Fix Z fighting with platform and build volume Contributes to issue CURA-6701. --- resources/definitions/felixpro2dual.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/felixpro2dual.def.json b/resources/definitions/felixpro2dual.def.json index 37c576952e..5ec7b12a64 100644 --- a/resources/definitions/felixpro2dual.def.json +++ b/resources/definitions/felixpro2dual.def.json @@ -8,7 +8,7 @@ "manufacturer": "Felix", "platform": "FelixPro2_platform.obj", "platform_texture": "FelixPro2_platform.png", - "platform_offset": [-135, 0, 130], + "platform_offset": [-135, -0.5, 130], "machine_extruder_trains": { "0": "felixpro2_dual_extruder_0", @@ -44,7 +44,7 @@ [ 70, -50 ] ] }, - "gantry_height": { "value": "0" }, + "gantry_height": { "value": "0" }, "machine_extruder_count": { "default_value": 2 }, "prime_tower_position_x": { "value": "250" }, "prime_tower_position_y": { "value": "200" }, From 58bb2097ce189b81250d822395263185c973fb28 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 5 Aug 2019 10:35:55 +0200 Subject: [PATCH 58/60] Remove Felix' platform texture It didn't work out well with our theming and the colours of objects on the build plate, and was inconsistent with how other printers' build plates look. Contributes to issue CURA-6701. --- resources/definitions/felixpro2dual.def.json | 1 - resources/images/FelixPro2_platform.png | Bin 4481 -> 0 bytes 2 files changed, 1 deletion(-) delete mode 100644 resources/images/FelixPro2_platform.png diff --git a/resources/definitions/felixpro2dual.def.json b/resources/definitions/felixpro2dual.def.json index 5ec7b12a64..0c978cdb71 100644 --- a/resources/definitions/felixpro2dual.def.json +++ b/resources/definitions/felixpro2dual.def.json @@ -7,7 +7,6 @@ "author": "pnks", "manufacturer": "Felix", "platform": "FelixPro2_platform.obj", - "platform_texture": "FelixPro2_platform.png", "platform_offset": [-135, -0.5, 130], "machine_extruder_trains": { diff --git a/resources/images/FelixPro2_platform.png b/resources/images/FelixPro2_platform.png deleted file mode 100644 index be88994a40065d65a6b0bf9679ba447dea52b204..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4481 zcmeAS@N?(olHy`uVBq!ia0y~yU;#3j7&zE~l+0PVoj?K3s*s41pu}>8f};Gi%$!t( zlFEWqh1817GzNx>TWiCk%Ve~9{#@63Cp6(mv8B!VlYE>0L|^z)k(N}RCS%T*5T)x9 z>Urc!{eP)1@^XfcPVN$@{dDia#*=C%XSfvGRDR8Q^XmNZcURj>57|c=ADonQ<+1CR z-<$hNBR0JGTGcaWzzK9iF>3&ANJ8 zaOoqCStiLmFG82!%_(*b*KapwO66euyk(=iGwYYlwqN`^<$NYsam7)?~7`%HWe5;J1 zW5ZTwgEa{UD#DJ-39?Q+z@wndaf&1MiWejEn>i*dnqgO)Wu6#(J7(TEeWiuvv_PJ# zldQgaoUjNMPKkWIV19zK$&|TPujhQ$+wo%3>A1gV4Bj`-4>e`yHQVvPnPZ|8mu1$L zi>~s{J%&f;sA#TQ=Y8V1NU)b>@@1a$lJV&hPhys2Y&KPc1+>T$(fxwYki)nrK@pnI>gyjGI`Fi$%Y0Q z*4@%IkMoS(9X`5OEEW*Y|Fr(h#U<}tpZ$CC*7(}3J@Z~Cz418AxhgQ_{)H*3kJz_A zyRIxd`C-gs1FIFEtlCzuQQkRW4!7m^h+{LRWI2{ScJp2QEF;BaTVC9X7cz;58iJN* z8hvPgaOl&^!mPZYuH3SHZxgIqEQ_!9G9KG?B|-7bqIH4S7v5STb^i&+)+;L}zu7PU zyiUcJooQ*fanRr2<^CFHeyH(%VK~yp_-sD&$Hv2MjQ>~Ou(-f1pw7e1lJJe=uqxLV zHlFDk7q|`fsIdLwYAHYJkbj_{vZ{)?&0FL}An$!~X4_Q{er0tmjUwAF zE#CJXjqAJ)TZ4r!dxS52C~J5jG_%5;=ZiCI&`WvlC%nv6f9LHGW8PcmAJg4fx5B{< zjla&Qh@H9PK>VaH_J@`|y|CN4e;oq@V{4|fbAYF_GptYm76o%^C)#=(c91z5AG}ma zR8YG^BV~o3n5(PI(k|Bx0k^o;TFr?0rD&4r5fmfCdcCPxzB`m%JUv!6hSl){^O2)h zJukX?{fQ6C5PW>F(Ei@t>N|UxTcSEwrrow!kTq>$kLyvNGFP5GN)3u%5_xQejK5Z7 zDe|8GSjsOS|6a-F^zS?K7ki(a$@8J}$YvjNokyjQUix}J-{UWn!MP|ku_QG`p**uBL&4qC zHz2%`Pn>~)+u75_F{I+w+Z(qz4>>Tn9^8HN$AOY9Uvrwn62vndSgbo-TvrP;otaW# z=byQ@mg#`nmwT=c-rvc;er9^{vX4_&+7(M(mw7$w@P`xo^lna7UAm{0sX<;^1!z43 zGl(z%5d@hBz^ZsaBta%|CPJ(v&O{_DX=OMGVL`gfNiZDAt3)zsWH>1S4OUB}bHOoA zis7S4gaHH)(M(D}L)0SlA{m37Ns8g5#NucY0mdO}EKUFkkdnGbqY)Hh2S5ZQ+(=1) zGz#d^$^dJsM2aMWOj^YvO=}vgzQ$gpjwTVTfkiM~ASs|Ei6rda_w Date: Mon, 5 Aug 2019 10:43:49 +0200 Subject: [PATCH 59/60] Fix manufacturer of Monoprice Ultimate Fixes #6104. --- .../definitions/monoprice_ultimate.def.json | 92 +++++++++---------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/resources/definitions/monoprice_ultimate.def.json b/resources/definitions/monoprice_ultimate.def.json index 48290f0941..445347b54e 100644 --- a/resources/definitions/monoprice_ultimate.def.json +++ b/resources/definitions/monoprice_ultimate.def.json @@ -1,52 +1,48 @@ { - "version": 2, - "name": "Monoprice Ultimate", - "inherits": "wanhao_d6", - "metadata": { - "visible": true, - "author": "Danny Tuppeny", - "manufacturer": "monoprice", - "file_formats": "text/x-gcode", - "icon": "wanhao-icon.png", - "has_materials": true, - "platform": "wanhao_200_200_platform.obj", - "platform_texture": "Wanhaobackplate.png", - "machine_extruder_trains": { - "0": "wanhao_d6_extruder_0" + "version": 2, + "name": "Monoprice Ultimate", + "inherits": "wanhao_d6", + "metadata": { + "visible": true, + "author": "Danny Tuppeny", + "manufacturer": "Monoprice", + "file_formats": "text/x-gcode", + "icon": "wanhao-icon.png", + "has_materials": true, + "platform": "wanhao_200_200_platform.obj", + "platform_texture": "Wanhaobackplate.png", + "machine_extruder_trains": { + "0": "wanhao_d6_extruder_0" + }, + "platform_offset": [0, -28, 0] }, - "platform_offset": [ - 0, - -28, - 0 - ] - }, - "overrides": { - "machine_name": { - "default_value": "Monoprice Ultimate" - }, - "machine_max_acceleration_x": { - "default_value": 3000 - }, - "machine_max_acceleration_y": { - "default_value": 3000 - }, - "machine_max_acceleration_z": { - "default_value": 100 - }, - "machine_max_acceleration_e": { - "default_value": 500 - }, - "machine_acceleration": { - "default_value": 800 - }, - "machine_max_jerk_xy": { - "default_value": 10.0 - }, - "machine_max_jerk_z": { - "default_value": 0.4 - }, - "machine_max_jerk_e": { - "default_value": 1.0 + "overrides": { + "machine_name": { + "default_value": "Monoprice Ultimate" + }, + "machine_max_acceleration_x": { + "default_value": 3000 + }, + "machine_max_acceleration_y": { + "default_value": 3000 + }, + "machine_max_acceleration_z": { + "default_value": 100 + }, + "machine_max_acceleration_e": { + "default_value": 500 + }, + "machine_acceleration": { + "default_value": 800 + }, + "machine_max_jerk_xy": { + "default_value": 10.0 + }, + "machine_max_jerk_z": { + "default_value": 0.4 + }, + "machine_max_jerk_e": { + "default_value": 1.0 + } } - } } From bb34e0da39c0399abb208cdc2a33a2656477da82 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 5 Aug 2019 10:44:20 +0200 Subject: [PATCH 60/60] Increase axis line width --- cura/BuildVolume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index ed211ed7b4..6bcc5988f9 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -64,7 +64,7 @@ class BuildVolume(SceneNode): self._origin_mesh = None # type: Optional[MeshData] self._origin_line_length = 20 - self._origin_line_width = 0.5 + self._origin_line_width = 1.5 self._grid_mesh = None # type: Optional[MeshData] self._grid_shader = None