From efe4d3734dfe10856999ed03f42b86aaaa25ac04 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 7 Jun 2018 15:49:23 +0200 Subject: [PATCH 01/23] Add documentation Done during issue CURA-5034. --- plugins/XmlMaterialProfile/XmlMaterialValidator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/XmlMaterialProfile/XmlMaterialValidator.py b/plugins/XmlMaterialProfile/XmlMaterialValidator.py index f11c8bea4b..a23022854b 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialValidator.py +++ b/plugins/XmlMaterialProfile/XmlMaterialValidator.py @@ -1,12 +1,13 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Any, Dict - -class XmlMaterialValidator(): - +## Makes sure that the required metadata is present for a material. +class XmlMaterialValidator: + ## Makes sure that the required metadata is present for a material. @classmethod - def validateMaterialMetaData(cls, validation_metadata): + def validateMaterialMetaData(cls, validation_metadata: Dict[str, Any]): if validation_metadata.get("GUID") is None: return "Missing GUID" From 4c1149643f2110c890c6ed04c827387ab5cefb76 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 13:08:34 +0200 Subject: [PATCH 02/23] Remove unused import Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c54ced6b13..1492618718 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -198,8 +198,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): yield True #Return that we had success! yield #To prevent having to catch the StopIteration exception. - from cura.Utils.Threading import call_on_qt_thread - def _sendPrintJobWaitOnWriteJobFinished(self, job): self._write_job_progress_message.hide() From 82a34b49ebcd32d542d9c854bcb27b6ffdb30f10 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 13:55:29 +0200 Subject: [PATCH 03/23] Add function to send material profiles to cluster This only works for cluster. Contributes to issue CURA-5034. --- .../ClusterUM3OutputDevice.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 1492618718..4d3b065659 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -25,7 +25,7 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject -from time import time, sleep +from time import time from datetime import datetime from typing import Optional, Dict, List @@ -91,6 +91,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) + self.sendMaterialProfiles() + #Formats supported by this application (file types that we can actually write). if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() @@ -533,6 +535,28 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._active_printer = None self.activePrinterChanged.emit() + ## Sync the material profiles in Cura with the printer. + # + # This gets called when connecting to a printer as well as when sending a + # print. + # + # TODO: For now we send all material profiles that are fully loaded. See + # if that has any performance impact. If not, we can try sending ALL + # profiles. If it has, we may need to limit it to the profiles that are + # active in the current machine. + def sendMaterialProfiles(self): + container_registry = ContainerRegistry.getInstance() + + base_files = set() + for material_metadata in container_registry.findContainersMetadata(type = "material"): + if container_registry.isLoaded(material_metadata["id"]): + if "base_file" not in material_metadata: + continue #If there's no base file then there was no file for it (such as empty_material). + base_files.add(material_metadata["base_file"]) + + for file in base_files: + Logger.log("d", "Syncing material profile with printer: {file}".format(file = file)) + #TODO Call API to send profile. def loadJsonFromReply(reply): try: From 3b510af8f53162969aa8513c4d6d3c6f81485e54 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 14:31:19 +0200 Subject: [PATCH 04/23] Add default 'None' for onFinished I don't always need to know whether it was successful. Contributes to issue CURA-5034. --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 97702ba08b..5f5bcf7935 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -213,7 +213,14 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply.uploadProgress.connect(onProgress) self._registerOnFinishedCallback(reply, onFinished) - def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: + ## Send an API call to the printer via HTTP POST. + # \param target The target URL on the printer. + # \param parts The parts of a form. One must be provided for each form + # element in the POST call. Create form parts using _createFormPart. + # \param onFinished A function to call when the call has been completed. + # \param onProgress A function to call when progress has been made. Use + # this to update a progress bar. + def postFormWithParts(self, target: str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]] = None, onProgress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target, content_type=None) From 63f5c8346a9c3a4cb331e962d67afe2c695d0949 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 14:33:13 +0200 Subject: [PATCH 05/23] Implement API call to send material profile It seems to work now. I don't know if the material actually arrives but from this end it seems to send it at least. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 4d3b065659..4e05847dad 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -544,7 +544,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # if that has any performance impact. If not, we can try sending ALL # profiles. If it has, we may need to limit it to the profiles that are # active in the current machine. - def sendMaterialProfiles(self): + def sendMaterialProfiles(self) -> None: container_registry = ContainerRegistry.getInstance() base_files = set() @@ -556,7 +556,14 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): for file in base_files: Logger.log("d", "Syncing material profile with printer: {file}".format(file = file)) - #TODO Call API to send profile. + + parts = [] + material = container_registry.findContainers(id = file)[0] + serialized_material = material.serialize().encode("utf-8") + parts.append(self._createFormPart("name=\"file\"; filename=\"{file_name}.xml.fdm_material\"".format(file_name = file), serialized_material)) + parts.append(self._createFormPart("name=\"filename\"", (file + ".xml.fdm_material").encode("utf-8"), "text/plain")) + + self.postFormWithParts(target = "/materials", parts = parts) def loadJsonFromReply(reply): try: From ddb80be1ec1c290eccdf37805e876c3ebef07fd3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 15:06:06 +0200 Subject: [PATCH 06/23] Send materials asynchronously This way we won't block the interface while those materials are sending. Contributes to issue CURA-5034. --- .../ClusterUM3OutputDevice.py | 16 +++------ plugins/UM3NetworkPrinting/SendMaterialJob.py | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/SendMaterialJob.py diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 4e05847dad..2e07b09593 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -4,6 +4,7 @@ from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary). from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously. from UM.Logger import Logger +from UM.JobQueue import JobQueue #To send material profiles in the background. from UM.Application import Application from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog @@ -20,6 +21,7 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.NetworkCamera import NetworkCamera from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController +from .SendMaterialJob import SendMaterialJob from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices @@ -27,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject from time import time from datetime import datetime -from typing import Optional, Dict, List +from typing import Optional, Dict, List, Set import io #To create the correct buffers for sending data to the printer. import json @@ -554,16 +556,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): continue #If there's no base file then there was no file for it (such as empty_material). base_files.add(material_metadata["base_file"]) - for file in base_files: - Logger.log("d", "Syncing material profile with printer: {file}".format(file = file)) - - parts = [] - material = container_registry.findContainers(id = file)[0] - serialized_material = material.serialize().encode("utf-8") - parts.append(self._createFormPart("name=\"file\"; filename=\"{file_name}.xml.fdm_material\"".format(file_name = file), serialized_material)) - parts.append(self._createFormPart("name=\"filename\"", (file + ".xml.fdm_material").encode("utf-8"), "text/plain")) - - self.postFormWithParts(target = "/materials", parts = parts) + job = SendMaterialJob(material_ids = base_files, device = self) + job.run() def loadJsonFromReply(reply): try: diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py new file mode 100644 index 0000000000..72469f43e7 --- /dev/null +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -0,0 +1,33 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Set, TYPE_CHECKING + +from UM.Settings.ContainerRegistry import ContainerRegistry #To get the material profiles we need to send. +from UM.Job import Job #The interface we're implementing. +from UM.Logger import Logger + +if TYPE_CHECKING: + from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + +## Asynchronous job to send material profiles to the printer. +# +# This way it won't freeze up the interface while sending those materials. +class SendMaterialJob(Job): + def __init__(self, material_ids: Set[str], device: "NetworkedPrinterDevice"): + super().__init__() + self.material_ids = material_ids + self.device = device + + def run(self): + container_registry = ContainerRegistry.getInstance() + for material_id in self.material_ids: + Logger.log("d", "Syncing material profile with printer: {material_id}".format(material_id = material_id)) + + parts = [] + material = container_registry.findContainers(id = material_id)[0] + serialized_material = material.serialize().encode("utf-8") + parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}.xml.fdm_material\"".format(file_name = material_id), serialized_material)) + parts.append(self.device._createFormPart("name=\"filename\"", (material_id + ".xml.fdm_material").encode("utf-8"), "text/plain")) + + self.device.postFormWithParts(target = "/materials", parts = parts) \ No newline at end of file From 4727340ae448b79984a7214e4b32bd3dbb6f5370 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 16:02:00 +0200 Subject: [PATCH 07/23] Send only original files When we re-serialize them, the XML file will not be exactly the same so the signature will fail to match. We need to send the original bytes. This changes the behaviour: It now sends all non-default files instead of all files that were loaded fully. Contributes to issue CURA-5034. --- .../ClusterUM3OutputDevice.py | 16 +---------- plugins/UM3NetworkPrinting/SendMaterialJob.py | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 2e07b09593..19110bd11e 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -541,22 +541,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # # This gets called when connecting to a printer as well as when sending a # print. - # - # TODO: For now we send all material profiles that are fully loaded. See - # if that has any performance impact. If not, we can try sending ALL - # profiles. If it has, we may need to limit it to the profiles that are - # active in the current machine. def sendMaterialProfiles(self) -> None: - container_registry = ContainerRegistry.getInstance() - - base_files = set() - for material_metadata in container_registry.findContainersMetadata(type = "material"): - if container_registry.isLoaded(material_metadata["id"]): - if "base_file" not in material_metadata: - continue #If there's no base file then there was no file for it (such as empty_material). - base_files.add(material_metadata["base_file"]) - - job = SendMaterialJob(material_ids = base_files, device = self) + job = SendMaterialJob(device = self) job.run() def loadJsonFromReply(reply): diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index 72469f43e7..19b7598bef 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -1,11 +1,15 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Set, TYPE_CHECKING +import os #To walk over material files. +import os.path #To filter on material files. +from typing import TYPE_CHECKING -from UM.Settings.ContainerRegistry import ContainerRegistry #To get the material profiles we need to send. from UM.Job import Job #The interface we're implementing. from UM.Logger import Logger +from UM.Resources import Resources + +from cura.CuraApplication import CuraApplication if TYPE_CHECKING: from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice @@ -14,20 +18,19 @@ if TYPE_CHECKING: # # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): - def __init__(self, material_ids: Set[str], device: "NetworkedPrinterDevice"): + def __init__(self, device: "NetworkedPrinterDevice"): super().__init__() - self.material_ids = material_ids self.device = device def run(self): - container_registry = ContainerRegistry.getInstance() - for material_id in self.material_ids: - Logger.log("d", "Syncing material profile with printer: {material_id}".format(material_id = material_id)) - + for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): + if not file_path.startswith(Resources.getDataStoragePath() + os.sep): + continue + _, file_name = os.path.split(file_path) + Logger.log("d", "Syncing material profile with printer: {file_name}".format(file_name = file_name)) parts = [] - material = container_registry.findContainers(id = material_id)[0] - serialized_material = material.serialize().encode("utf-8") - parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}.xml.fdm_material\"".format(file_name = material_id), serialized_material)) - parts.append(self.device._createFormPart("name=\"filename\"", (material_id + ".xml.fdm_material").encode("utf-8"), "text/plain")) + with open(file_path, "rb") as f: + parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) + parts.append(self.device._createFormPart("name=\"filename\"", file_name.encode("utf-8"), "text/plain")) self.device.postFormWithParts(target = "/materials", parts = parts) \ No newline at end of file From 4e46963f58bcc04c5ca2fd19f2d19949024de35d Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 8 Jun 2018 16:07:17 +0200 Subject: [PATCH 08/23] Also send signature file if it exists The files are not placed there yet when unpacking a package, as far as I know. Next time. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index 19b7598bef..fb4d451097 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -28,9 +28,17 @@ class SendMaterialJob(Job): continue _, file_name = os.path.split(file_path) Logger.log("d", "Syncing material profile with printer: {file_name}".format(file_name = file_name)) + parts = [] with open(file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) parts.append(self.device._createFormPart("name=\"filename\"", file_name.encode("utf-8"), "text/plain")) + without_extension, _ = os.path.splitext(file_path) + signature_file_path = without_extension + ".sig" + if os.path.exists(signature_file_path): + _, signature_file_name = os.path.split(signature_file_path) + with open(signature_file_path, "rb") as f: + parts.append(self.device._createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) + self.device.postFormWithParts(target = "/materials", parts = parts) \ No newline at end of file From abf77da4c4f8409d7639e3e30e4dd0954c82f6f6 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 11 Jun 2018 15:55:49 +0200 Subject: [PATCH 09/23] Send to cluster API Not the normal printer API. That won't work without authentication. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index fb4d451097..bfcc17a690 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: # # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): - def __init__(self, device: "NetworkedPrinterDevice"): + def __init__(self, device: "ClusterUM3OutputDevice"): super().__init__() self.device = device @@ -32,7 +32,6 @@ class SendMaterialJob(Job): parts = [] with open(file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) - parts.append(self.device._createFormPart("name=\"filename\"", file_name.encode("utf-8"), "text/plain")) without_extension, _ = os.path.splitext(file_path) signature_file_path = without_extension + ".sig" @@ -41,4 +40,4 @@ class SendMaterialJob(Job): with open(signature_file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) - self.device.postFormWithParts(target = "/materials", parts = parts) \ No newline at end of file + self.device.postFormWithParts(target = self.device._api_prefix + "materials/", parts = parts) \ No newline at end of file From 0da7ad3946c740ff2ddad498ab61a2ba87834be3 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 11 Jun 2018 16:01:06 +0200 Subject: [PATCH 10/23] Use material.fdm_material.xml.sig for signature file names Just put the .sig after the entire file instead of replacing the extension. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index bfcc17a690..def218666a 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -33,8 +33,7 @@ class SendMaterialJob(Job): with open(file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) - without_extension, _ = os.path.splitext(file_path) - signature_file_path = without_extension + ".sig" + signature_file_path = file_path + ".sig" if os.path.exists(signature_file_path): _, signature_file_name = os.path.split(signature_file_path) with open(signature_file_path, "rb") as f: From 2dcb185a328506bc88e382da106c418b9bead706 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 12:50:33 +0200 Subject: [PATCH 11/23] First ask the printer which profiles it already has Only let the printer have one profile per GUID and make sure it's the one with the highest version. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 62 ++++++++++++++++--- .../XmlMaterialProfile/XmlMaterialProfile.py | 8 +-- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index def218666a..1904b1434b 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -1,18 +1,23 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import json #To understand the list of materials from the printer reply. import os #To walk over material files. import os.path #To filter on material files. +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest #To listen to the reply from the printer. from typing import TYPE_CHECKING +import urllib.parse #For getting material IDs from their file names. from UM.Job import Job #The interface we're implementing. from UM.Logger import Logger +from UM.MimeTypeDatabase import MimeTypeDatabase #To strip the extensions of the material profile files. from UM.Resources import Resources +from UM.Settings.ContainerRegistry import ContainerRegistry #To find the GUIDs of materials. -from cura.CuraApplication import CuraApplication +from cura.CuraApplication import CuraApplication #For the resource types. if TYPE_CHECKING: - from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + from .ClusterUM3OutputDevice import ClusterUM3OutputDevice ## Asynchronous job to send material profiles to the printer. # @@ -20,23 +25,62 @@ if TYPE_CHECKING: class SendMaterialJob(Job): def __init__(self, device: "ClusterUM3OutputDevice"): super().__init__() - self.device = device + self.device = device #type: ClusterUM3OutputDevice - def run(self): + def run(self) -> None: + self.device.get("materials/", onFinished = self.sendMissingMaterials) + + def sendMissingMaterials(self, reply: QNetworkReply) -> None: + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: #Got an error from the HTTP request. + Logger.log("e", "Couldn't request current material storage on printer. Not syncing materials.") + return + + remote_materials_list = reply.readAll().data().decode("utf-8") + try: + remote_materials_list = json.loads(remote_materials_list) + except json.JSONDecodeError: + Logger.log("e", "Current material storage on printer was a corrupted reply.") + return + try: + remote_materials_by_guid = {material["guid"]: material for material in remote_materials_list} #Index by GUID. + except KeyError: + Logger.log("e", "Current material storage on printer was an invalid reply (missing GUIDs).") + return + + container_registry = ContainerRegistry.getInstance() for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): - if not file_path.startswith(Resources.getDataStoragePath() + os.sep): + if not file_path.startswith(Resources.getDataStoragePath() + os.sep): #No built-in profiles. continue + mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) _, file_name = os.path.split(file_path) - Logger.log("d", "Syncing material profile with printer: {file_name}".format(file_name = file_name)) - + material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name)) + material_metadata = container_registry.findContainersMetadata(id = material_id) + if len(material_metadata) == 0: #This profile is not loaded. It's probably corrupt and deactivated. Don't send it. + continue + material_metadata = material_metadata[0] + if "GUID" not in material_metadata or "version" not in material_metadata: #Missing metadata? Faulty profile. + continue + material_guid = material_metadata["GUID"] + material_version = material_metadata["version"] + if material_guid in remote_materials_by_guid: + if "version" not in remote_materials_by_guid: + Logger.log("e", "Current material storage on printer was an invalid reply (missing version).") + return + if remote_materials_by_guid[material_guid]["version"] >= material_version: #Printer already knows this material and is up to date. + continue parts = [] with open(file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) - signature_file_path = file_path + ".sig" if os.path.exists(signature_file_path): _, signature_file_name = os.path.split(signature_file_path) with open(signature_file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) - self.device.postFormWithParts(target = self.device._api_prefix + "materials/", parts = parts) \ No newline at end of file + Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) + self.device.postFormWithParts(target = "materials/", parts = parts, onFinished = self.sendingFinished) + + def sendingFinished(self, reply: QNetworkReply): + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + Logger.log("e", "Received error code from printer when syncing material: {code}".format(code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) + Logger.log("e", reply.readAll().data().decode("utf-8")) \ No newline at end of file diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 148471fb6d..dfb07a1129 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -331,9 +331,9 @@ class XmlMaterialProfile(InstanceContainer): stream = io.BytesIO() tree = ET.ElementTree(root) # this makes sure that the XML header states encoding="utf-8" - tree.write(stream, encoding="utf-8", xml_declaration=True) + tree.write(stream, encoding = "utf-8", xml_declaration=True) - return stream.getvalue().decode('utf-8') + return stream.getvalue().decode("utf-8") # Recursively resolve loading inherited files def _resolveInheritance(self, file_name): @@ -349,7 +349,7 @@ class XmlMaterialProfile(InstanceContainer): def _loadFile(self, file_name): path = Resources.getPath(CuraApplication.getInstance().ResourceTypes.MaterialInstanceContainer, file_name + ".xml.fdm_material") - with open(path, encoding="utf-8") as f: + with open(path, encoding = "utf-8") as f: contents = f.read() self._inherited_files.append(path) @@ -955,7 +955,7 @@ class XmlMaterialProfile(InstanceContainer): def _addSettingElement(self, builder, instance): key = instance.definition.key if key in self.__material_settings_setting_map.values(): - # Setting has a key in the stabndard namespace + # Setting has a key in the standard namespace key = UM.Dictionary.findKey(self.__material_settings_setting_map, instance.definition.key) tag_name = "setting" elif key not in self.__material_properties_setting_map.values() and key not in self.__material_metadata_setting_map.values(): From 0a557fdb797d3e49dcb5211cd85f7ed21e00d45e Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 13:42:03 +0200 Subject: [PATCH 12/23] Send material per GUID with highest version number If you have multiple materials with the same GUID (in the case of custom profiles) then we want to send only the material with the highest version number and only the base material. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index 1904b1434b..690a0dc0e6 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -5,7 +5,7 @@ import json #To understand the list of materials from the printer reply. import os #To walk over material files. import os.path #To filter on material files. from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest #To listen to the reply from the printer. -from typing import TYPE_CHECKING +from typing import Any, Dict, Set, TYPE_CHECKING import urllib.parse #For getting material IDs from their file names. from UM.Job import Job #The interface we're implementing. @@ -48,26 +48,38 @@ class SendMaterialJob(Job): return container_registry = ContainerRegistry.getInstance() + local_materials_list = filter(lambda material: ("GUID" in material and "version" in material and "id" in material), container_registry.findContainersMetadata(type = "material")) + local_materials_by_guid = {material["GUID"]: material for material in local_materials_list if material["id"] == material["base_file"]} + for material in local_materials_list: #For each GUID get the material with the highest version number. + try: + if int(material["version"]) > local_materials_by_guid[material["GUID"]]["version"]: + local_materials_by_guid[material["GUID"]] = material + except ValueError: + Logger.log("e", "Material {material_id} has invalid version number {number}.".format(material_id = material["id"], number = material["version"])) + continue + + materials_to_send = set() #type: Set[Dict[str, Any]] + for guid, material in local_materials_by_guid.items(): + if guid not in remote_materials_by_guid: + materials_to_send.add(material["id"]) + continue + try: + if int(material["version"]) > remote_materials_by_guid[guid]["version"]: + materials_to_send.add(material["id"]) + continue + except KeyError: + Logger.log("e", "Current material storage on printer was an invalid reply (missing version).") + return + for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): if not file_path.startswith(Resources.getDataStoragePath() + os.sep): #No built-in profiles. continue mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) _, file_name = os.path.split(file_path) material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name)) - material_metadata = container_registry.findContainersMetadata(id = material_id) - if len(material_metadata) == 0: #This profile is not loaded. It's probably corrupt and deactivated. Don't send it. + if material_id not in materials_to_send: continue - material_metadata = material_metadata[0] - if "GUID" not in material_metadata or "version" not in material_metadata: #Missing metadata? Faulty profile. - continue - material_guid = material_metadata["GUID"] - material_version = material_metadata["version"] - if material_guid in remote_materials_by_guid: - if "version" not in remote_materials_by_guid: - Logger.log("e", "Current material storage on printer was an invalid reply (missing version).") - return - if remote_materials_by_guid[material_guid]["version"] >= material_version: #Printer already knows this material and is up to date. - continue + parts = [] with open(file_path, "rb") as f: parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) From 194bb5e8c85261e0c6db454525e6f8f2105962a4 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 13:45:58 +0200 Subject: [PATCH 13/23] Also send built-in profiles The printer firmware won't add them currently so we're sending these profiles to their doom. However a later firmware update might fix this so we can make older Cura versions send the profiles as well so that it is forward compatible. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index 690a0dc0e6..2dbfc5f164 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -72,9 +72,10 @@ class SendMaterialJob(Job): return for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): - if not file_path.startswith(Resources.getDataStoragePath() + os.sep): #No built-in profiles. - continue - mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) + try: + mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) + except MimeTypeDatabase.MimeTypeNotFoundError: + continue #Not the sort of file we'd like to send then. _, file_name = os.path.split(file_path) material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name)) if material_id not in materials_to_send: From a4171dd561c61dd1cd4687ed7259cc4c53053f99 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 13:56:21 +0200 Subject: [PATCH 14/23] Also sync when connecting with printer Though right now it seems to sync way too often. I don't know why it keeps connecting. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 19110bd11e..da3a98b1fc 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -4,7 +4,6 @@ from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary). from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously. from UM.Logger import Logger -from UM.JobQueue import JobQueue #To send material profiles in the background. from UM.Application import Application from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog @@ -19,6 +18,7 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.NetworkCamera import NetworkCamera +from cura.PrinterOutputDevice import ConnectionState #To see when we're fully connected. from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController from .SendMaterialJob import SendMaterialJob @@ -62,6 +62,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) + self.connectionStateChanged.connect(self._onConnectionStateChanged) + self._accepts_commands = True # Cluster does not have authentication, so default to authenticated @@ -368,6 +370,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # 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 _onConnectionStateChanged(self) -> None: + if self.connectionState == ConnectionState.connected: + self.sendMaterialProfiles() + def _update(self) -> None: if not super()._update(): return @@ -417,6 +424,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self.printJobsChanged.emit() # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: + self.connect() if not checkValidGetReply(reply): return From da5e4c11a445fce300eb37bdea9babd8d0715363 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 15:40:10 +0200 Subject: [PATCH 15/23] Override connect() rather than connecting to connection state signal This is much more lean and a bit more semantic. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index da3a98b1fc..1dc82a800f 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -62,8 +62,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) - self.connectionStateChanged.connect(self._onConnectionStateChanged) - self._accepts_commands = True # Cluster does not have authentication, so default to authenticated @@ -371,9 +369,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._finished_jobs = finished_jobs ## Called when the connection to the cluster changes. - def _onConnectionStateChanged(self) -> None: - if self.connectionState == ConnectionState.connected: - self.sendMaterialProfiles() + def connect(self) -> None: + super().connect() + self.sendMaterialProfiles() def _update(self) -> None: if not super()._update(): From d87166d5c2210b4ba47df22c81ee878d399ef7fe Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 15:40:47 +0200 Subject: [PATCH 16/23] Don't reconnect when getting printer data I think I accidentally added this during debugging at some point this sleepy morning. Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 1dc82a800f..0a98ea2ed2 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -422,7 +422,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self.printJobsChanged.emit() # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: - self.connect() if not checkValidGetReply(reply): return From 2e005a09acd222b9a3024c8cb34539b5d8801d9b Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 15:41:52 +0200 Subject: [PATCH 17/23] Remove unused import Contributes to issue CURA-5034. --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 0a98ea2ed2..51ba159990 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -18,7 +18,6 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.NetworkCamera import NetworkCamera -from cura.PrinterOutputDevice import ConnectionState #To see when we're fully connected. from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController from .SendMaterialJob import SendMaterialJob From b2cd0ffc0f496697ac59b3eb62160ef76ec7e838 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 15:44:22 +0200 Subject: [PATCH 18/23] Make createFormParts public Because the postFormWithParts function is public and it needs these parts that can (or should) only be created with this function. Contributes to issue CURA-5034. --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 2 +- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 6 +++--- plugins/UM3NetworkPrinting/SendMaterialJob.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 5f5bcf7935..df2675e83f 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -161,7 +161,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart: + def createFormPart(self, content_header, data, content_type = None) -> QHttpPart: part = QHttpPart() if not content_header.startswith("form-data;"): diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 51ba159990..0c3febc997 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -217,10 +217,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # 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")) + 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")) + parts.append(self.createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] @@ -228,7 +228,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if isinstance(stream, io.StringIO): output = output.encode("utf-8") - parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) + parts.append(self.createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index 2dbfc5f164..230f579b34 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -83,12 +83,12 @@ class SendMaterialJob(Job): parts = [] with open(file_path, "rb") as f: - parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) + parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) signature_file_path = file_path + ".sig" if os.path.exists(signature_file_path): _, signature_file_name = os.path.split(signature_file_path) with open(signature_file_path, "rb") as f: - parts.append(self.device._createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) + parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) self.device.postFormWithParts(target = "materials/", parts = parts, onFinished = self.sendingFinished) From d5878772e8edd70eeaf7b93bc49fc726005ce4ad Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 16:12:03 +0200 Subject: [PATCH 19/23] Fix reading flow temperature graph from XML files These have their own nice little subtags that we don't even parse. Contributes to issue CURA-5034. --- .../XmlMaterialProfile/XmlMaterialProfile.py | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index dfb07a1129..2d53a70efe 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -563,7 +563,16 @@ class XmlMaterialProfile(InstanceContainer): for entry in settings: key = entry.get("key") if key in self.__material_settings_setting_map: - common_setting_values[self.__material_settings_setting_map[key]] = entry.text + if key == "processing temperature graph": #This setting has no setting text but subtags. + graph_nodes = entry.iterfind("./um:point", self.__namespaces) + graph_points = [] + for graph_node in graph_nodes: + flow = float(graph_node.get("flow")) + temperature = float(graph_node.get("temperature")) + graph_points.append([flow, temperature]) + common_setting_values[self.__material_settings_setting_map[key]] = str(graph_points) + else: + common_setting_values[self.__material_settings_setting_map[key]] = entry.text elif key in self.__unmapped_settings: if key == "hardware compatible": common_compatibility = self._parseCompatibleValue(entry.text) @@ -596,7 +605,16 @@ class XmlMaterialProfile(InstanceContainer): for entry in settings: key = entry.get("key") if key in self.__material_settings_setting_map: - machine_setting_values[self.__material_settings_setting_map[key]] = entry.text + if key == "processing temperature graph": #This setting has no setting text but subtags. + graph_nodes = entry.iterfind("./um:point", self.__namespaces) + graph_points = [] + for graph_node in graph_nodes: + flow = float(graph_node.get("flow")) + temperature = float(graph_node.get("temperature")) + graph_points.append([flow, temperature]) + machine_setting_values[self.__material_settings_setting_map[key]] = str(graph_points) + else: + machine_setting_values[self.__material_settings_setting_map[key]] = entry.text elif key in self.__unmapped_settings: if key == "hardware compatible": machine_compatibility = self._parseCompatibleValue(entry.text) @@ -713,7 +731,16 @@ class XmlMaterialProfile(InstanceContainer): for entry in settings: key = entry.get("key") if key in self.__material_settings_setting_map: - hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text + if key == "processing temperature graph": #This setting has no setting text but subtags. + graph_nodes = entry.iterfind("./um:point", self.__namespaces) + graph_points = [] + for graph_node in graph_nodes: + flow = float(graph_node.get("flow")) + temperature = float(graph_node.get("temperature")) + graph_points.append([flow, temperature]) + hotend_setting_values[self.__material_settings_setting_map[key]] = str(graph_points) + else: + hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text elif key in self.__unmapped_settings: if key == "hardware compatible": hotend_compatibility = self._parseCompatibleValue(entry.text) From dd750ce125c09ad506b936d0ad47bf18838b5418 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 12 Jun 2018 16:37:59 +0200 Subject: [PATCH 20/23] Fix serializing flow-temperature graph This graph needs to have its own format in the XML material profiles, which wasn't implemented. Contributes to issue CURA-5034. --- plugins/XmlMaterialProfile/XmlMaterialProfile.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 2d53a70efe..01a79de57b 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -985,6 +985,18 @@ class XmlMaterialProfile(InstanceContainer): # Setting has a key in the standard namespace key = UM.Dictionary.findKey(self.__material_settings_setting_map, instance.definition.key) tag_name = "setting" + + if key == "processing temperature graph": #The Processing Temperature Graph has its own little structure that we need to implement separately. + builder.start(tag_name, {"key": key}) + graph_str = str(instance.value) + graph = graph_str.replace("[", "").replace("]", "").split(", ") #Crude parsing of this list: Flatten the list by removing all brackets, then split on ", ". Safe to eval attacks though! + graph = [graph[i:i + 2] for i in range(0, len(graph) - 1, 2)] #Convert to 2D array. + for point in graph: + builder.start("point", {"flow": point[0], "temperature": point[1]}) + builder.end("point") + builder.end(tag_name) + return + elif key not in self.__material_properties_setting_map.values() and key not in self.__material_metadata_setting_map.values(): # Setting is not in the standard namespace, and not a material property (eg diameter) or metadata (eg GUID) tag_name = "cura:setting" From b55abee783eaabdc1881047ea99fc3378e54c0bf Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 2 Jul 2018 13:05:22 +0200 Subject: [PATCH 21/23] Code style did not rename this variable --- plugins/UM3NetworkPrinting/SendMaterialJob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/SendMaterialJob.py b/plugins/UM3NetworkPrinting/SendMaterialJob.py index 230f579b34..135b2c421e 100644 --- a/plugins/UM3NetworkPrinting/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/SendMaterialJob.py @@ -28,7 +28,7 @@ class SendMaterialJob(Job): self.device = device #type: ClusterUM3OutputDevice def run(self) -> None: - self.device.get("materials/", onFinished = self.sendMissingMaterials) + self.device.get("materials/", on_finished = self.sendMissingMaterials) def sendMissingMaterials(self, reply: QNetworkReply) -> None: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: #Got an error from the HTTP request. From 680e98fb28e3d001938d88c8404d846119319b18 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 2 Jul 2018 13:11:01 +0200 Subject: [PATCH 22/23] Removed duplicate '.self' --- cura/Settings/MachineManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index a905ecdb42..a088592b88 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1288,7 +1288,7 @@ class MachineManager(QObject): @pyqtSlot(str) def switchPrinterType(self, machine_name: str) -> None: # Don't switch if the user tries to change to the same type of printer - if self._global_container_stack is None or self.self.activeMachineDefinitionName == machine_name: + if self._global_container_stack is None or self.activeMachineDefinitionName == machine_name: return # Get the definition id corresponding to this machine name machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId() From adc7fc58e9d2116f0c6f158d91134413d823fc90 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 2 Jul 2018 13:53:35 +0200 Subject: [PATCH 23/23] Fixed method naming conflict --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index f21cc23904..8b3ceb7809 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -220,10 +220,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # 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")) + 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")) + parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] @@ -232,7 +232,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): output = cast(str, output).encode("utf-8") output = cast(bytes, output) - parts.append(self.createFormPart("name=\"file\"; filename=\"%s\"" % file_name, 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)