diff --git a/cura/Machines/Models/CompatibleMachineModel.py b/cura/Machines/Models/CompatibleMachineModel.py index 40a3618b31..40369b89a7 100644 --- a/cura/Machines/Models/CompatibleMachineModel.py +++ b/cura/Machines/Models/CompatibleMachineModel.py @@ -51,6 +51,9 @@ class CompatibleMachineModel(ListModel): for output_device in machine_manager.printerOutputDevices: for printer in output_device.printers: extruder_configs = dict() + # If the printer name already exist in the queue skip it + if printer.name in [item["name"] for item in self.items]: + continue # initialize & add current active material: for extruder in printer.extruders: diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index d54092b8c9..1c68ecce92 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -40,9 +40,22 @@ class ExtruderConfigurationModel(QObject): def setHotendID(self, hotend_id: Optional[str]) -> None: if self._hotend_id != hotend_id: - self._hotend_id = hotend_id + self._hotend_id = ExtruderConfigurationModel.applyNameMappingHotend(hotend_id) self.extruderConfigurationChanged.emit() + @staticmethod + def applyNameMappingHotend(hotendId) -> str: + _EXTRUDER_NAME_MAP = { + "mk14_hot":"1XA", + "mk14_hot_s":"2XA", + "mk14_c":"1C", + "mk14":"1A", + "mk14_s":"2A" + } + if hotendId in _EXTRUDER_NAME_MAP: + return _EXTRUDER_NAME_MAP[hotendId] + return hotendId + @pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged) def hotendID(self) -> Optional[str]: return self._hotend_id diff --git a/cura/PrinterOutput/Models/MaterialOutputModel.py b/cura/PrinterOutput/Models/MaterialOutputModel.py index 89509ace72..6920cead1b 100644 --- a/cura/PrinterOutput/Models/MaterialOutputModel.py +++ b/cura/PrinterOutput/Models/MaterialOutputModel.py @@ -9,7 +9,9 @@ from PyQt6.QtCore import pyqtProperty, QObject class MaterialOutputModel(QObject): def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None: super().__init__(parent) - self._guid = guid + + name, guid = MaterialOutputModel.getMaterialFromDefinition(guid,type, brand, name) + self._guid =guid self._type = type self._color = color self._brand = brand @@ -19,6 +21,34 @@ class MaterialOutputModel(QObject): def guid(self) -> str: return self._guid if self._guid else "" + @staticmethod + def getMaterialFromDefinition(guid, type, brand, name): + + _MATERIAL_MAP = { "abs" :{"name" :"abs_175" ,"guid": "2780b345-577b-4a24-a2c5-12e6aad3e690"}, + "abs-wss1" :{"name" :"absr_175" ,"guid": "88c8919c-6a09-471a-b7b6-e801263d862d"}, + "asa" :{"name" :"asa_175" ,"guid": "416eead4-0d8e-4f0b-8bfc-a91a519befa5"}, + "nylon-cf" :{"name" :"cffpa_175" ,"guid": "85bbae0e-938d-46fb-989f-c9b3689dc4f0"}, + "nylon" :{"name" :"nylon_175" ,"guid": "283d439a-3490-4481-920c-c51d8cdecf9c"}, + "pc" :{"name" :"pc_175" ,"guid": "62414577-94d1-490d-b1e4-7ef3ec40db02"}, + "petg" :{"name" :"petg_175" ,"guid": "69386c85-5b6c-421a-bec5-aeb1fb33f060"}, + "pla" :{"name" :"pla_175" ,"guid": "0ff92885-617b-4144-a03c-9989872454bc"}, + "pva" :{"name" :"pva_175" ,"guid": "a4255da2-cb2a-4042-be49-4a83957a2f9a"}, + "wss1" :{"name" :"rapidrinse_175","guid": "a140ef8f-4f26-4e73-abe0-cfc29d6d1024"}, + "sr30" :{"name" :"sr30_175" ,"guid": "77873465-83a9-4283-bc44-4e542b8eb3eb"}, + "im-pla" :{"name" :"tough_pla_175" ,"guid": "96fca5d9-0371-4516-9e96-8e8182677f3c"}, + "bvoh" :{"name" :"bvoh_175" ,"guid": "923e604c-8432-4b09-96aa-9bbbd42207f4"}, + "cpe" :{"name" :"cpe_175" ,"guid": "da1872c1-b991-4795-80ad-bdac0f131726"}, + "hips" :{"name" :"hips_175" ,"guid": "a468d86a-220c-47eb-99a5-bbb47e514eb0"}, + "tpu" :{"name" :"tpu_175" ,"guid": "19baa6a9-94ff-478b-b4a1-8157b74358d2"} + } + + + if guid is None and brand is not "empty" and type in _MATERIAL_MAP: + name = _MATERIAL_MAP[type]["name"] + guid = _MATERIAL_MAP[type]["guid"] + return name, guid + + @pyqtProperty(str, constant = True) def type(self) -> str: return self._type diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 0fc387a53f..5c4b26632a 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -415,7 +415,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): @pyqtProperty(str, constant = True) def printerType(self) -> str: - return self._properties.get(b"printer_type", b"Unknown").decode("utf-8") + return NetworkedPrinterOutputDevice.applyPrinterTypeMapping(self._properties.get(b"printer_type", b"Unknown").decode("utf-8")) + + @staticmethod + def applyPrinterTypeMapping(printer_type): + _PRINTER_TYPE_NAME = { + "fire_e": "ultimaker_method", + "lava_f": "ultimaker_methodx", + "magma_10": "ultimaker_methodxl" + } + if printer_type in _PRINTER_TYPE_NAME: + return _PRINTER_TYPE_NAME[printer_type] + return printer_type @pyqtProperty(str, constant = True) def ipAddress(self) -> str: diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index a25a487c6e..8bc8939e19 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -284,16 +284,20 @@ class CuraStackBuilder: abstract_machines = registry.findContainerStacks(id = abstract_machine_id) if abstract_machines: return cast(GlobalStack, abstract_machines[0]) + definitions = registry.findDefinitionContainers(id=definition_id) name = "" - if definitions: name = definitions[0].getName() + stack = cls.createMachine(abstract_machine_id, definition_id, show_warning_message=False) if not stack: return None + if not stack.getMetaDataEntry("visible", True): + return None + stack.setName(name) stack.setMetaDataEntry("is_abstract_machine", True) diff --git a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml index 0a94a4f48a..0b79b77a08 100644 --- a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml @@ -208,12 +208,14 @@ Item anchors.rightMargin: UM.Theme.getSize("thin_margin").height enabled: UM.Backend.state == UM.Backend.Done - currentIndex: UM.Backend.state == UM.Backend.Done ? 0 : 1 + currentIndex: UM.Backend.state == UM.Backend.Done ? dfFilenameTextfield.text.startsWith("MM")? 1 : 0 : 2 + textRole: "text" valueRole: "value" model: [ - { text: catalog.i18nc("@option", "Save Cura project and print file"), key: "3mf_ufp", value: ["3mf", "ufp"] }, + { text: catalog.i18nc("@option", "Save Cura project and .ufp print file"), key: "3mf_ufp", value: ["3mf", "ufp"] }, + { text: catalog.i18nc("@option", "Save Cura project and .makerbot print file"), key: "3mf_makerbot", value: ["3mf", "makerbot"] }, { text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] }, ] } diff --git a/plugins/DigitalLibrary/src/DFFileExportAndUploadManager.py b/plugins/DigitalLibrary/src/DFFileExportAndUploadManager.py index 26912abd9a..271fe652c8 100644 --- a/plugins/DigitalLibrary/src/DFFileExportAndUploadManager.py +++ b/plugins/DigitalLibrary/src/DFFileExportAndUploadManager.py @@ -27,7 +27,7 @@ from .ExportFileJob import ExportFileJob class DFFileExportAndUploadManager: """ Class responsible for exporting the scene and uploading the exported data to the Digital Factory Library. Since 3mf - and UFP files may need to be uploaded at the same time, this class keeps a single progress and success message for + and (UFP or makerbot) files may need to be uploaded at the same time, this class keeps a single progress and success message for both files and updates those messages according to the progress of both the file job uploads. """ def __init__(self, file_handlers: Dict[str, FileHandler], @@ -118,7 +118,7 @@ class DFFileExportAndUploadManager: library_project_id = self._library_project_id, source_file_id = self._source_file_id ) - self._api.requestUploadUFP(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed) + self._api.requestUploadMeshFile(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed) def _uploadFileData(self, file_upload_response: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse]) -> None: """Uploads the exported file data after the file or print job upload has been registered at the Digital Factory @@ -279,22 +279,25 @@ class DFFileExportAndUploadManager: This means that something went wrong with the initial request to create a "file" entry in the digital library. """ reply_string = bytes(reply.readAll()).decode() - filename_ufp = self._file_name + ".ufp" - Logger.log("d", "An error occurred while uploading the print job file '{}' to the Digital Library project '{}': {}".format(filename_ufp, self._library_project_id, reply_string)) + if "ufp" in self._formats: + filename_meshfile = self._file_name + ".ufp" + elif "makerbot" in self._formats: + filename_meshfile = self._file_name + ".makerbot" + Logger.log("d", "An error occurred while uploading the print job file '{}' to the Digital Library project '{}': {}".format(filename_meshfile, self._library_project_id, reply_string)) with self._message_lock: # Set the progress to 100% when the upload job fails, to avoid having the progress message stuck - self._file_upload_job_metadata[filename_ufp]["upload_status"] = "failed" - self._file_upload_job_metadata[filename_ufp]["upload_progress"] = 100 + self._file_upload_job_metadata[filename_meshfile]["upload_status"] = "failed" + self._file_upload_job_metadata[filename_meshfile]["upload_progress"] = 100 human_readable_error = self.extractErrorTitle(reply_string) - self._file_upload_job_metadata[filename_ufp]["file_upload_failed_message"] = getBackwardsCompatibleMessage( + self._file_upload_job_metadata[filename_meshfile]["file_upload_failed_message"] = getBackwardsCompatibleMessage( title = "File upload error", - text = "Failed to upload the file '{}' to '{}'. {}".format(filename_ufp, self._library_project_name, human_readable_error), + text = "Failed to upload the file '{}' to '{}'. {}".format(filename_meshfile, self._library_project_name, human_readable_error), message_type_str = "ERROR", lifetime = 30 ) self._on_upload_error() - self._onFileUploadFinished(filename_ufp) + self._onFileUploadFinished(filename_meshfile) @staticmethod def extractErrorTitle(reply_body: Optional[str]) -> str: @@ -407,4 +410,28 @@ class DFFileExportAndUploadManager: job_ufp = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp") job_ufp.finished.connect(self._onPrintFileExported) self._upload_jobs.append(job_ufp) + + if "makerbot" in self._formats and "makerbot" in self._file_handlers and self._file_handlers["makerbot"]: + filename_makerbot = self._file_name + ".makerbot" + metadata[filename_makerbot] = { + "export_job_output" : None, + "upload_progress" : -1, + "upload_status" : "", + "file_upload_response": None, + "file_upload_success_message": getBackwardsCompatibleMessage( + text = "'{}' was uploaded to '{}'.".format(filename_makerbot, self._library_project_name), + title = "Upload successful", + message_type_str = "POSITIVE", + lifetime = 30, + ), + "file_upload_failed_message": getBackwardsCompatibleMessage( + text = "Failed to upload the file '{}' to '{}'.".format(filename_makerbot, self._library_project_name), + title = "File upload error", + message_type_str = "ERROR", + lifetime = 30 + ) + } + job_makerbot = ExportFileJob(self._file_handlers["makerbot"], self._nodes, self._file_name, "makerbot") + job_makerbot.finished.connect(self._onPrintFileExported) + self._upload_jobs.append(job_makerbot) return metadata diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index 13c65f79c4..1168928588 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -313,7 +313,7 @@ class DigitalFactoryApiClient: error_callback = on_error, timeout = self.DEFAULT_REQUEST_TIMEOUT) - def requestUploadUFP(self, request: DFPrintJobUploadRequest, + def requestUploadMeshFile(self, request: DFPrintJobUploadRequest, on_finished: Callable[[DFPrintJobUploadResponse], Any], on_error: Optional[Callable[["QNetworkReply", "QNetworkReply.NetworkError"], None]] = None) -> None: """Requests the Digital Factory to register the upload of a file in a library project. diff --git a/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py b/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py index 0a10ea034c..0ed8f491f9 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py @@ -92,7 +92,8 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice): if not self._controller.file_handlers: self._controller.file_handlers = { "3mf": CuraApplication.getInstance().getWorkspaceFileHandler(), - "ufp": CuraApplication.getInstance().getMeshFileHandler() + "ufp": CuraApplication.getInstance().getMeshFileHandler(), + "makerbot": CuraApplication.getInstance().getMeshFileHandler() } self._dialog = CuraApplication.getInstance().createQmlComponent(self._dialog_path, {"manager": self._controller}) diff --git a/plugins/UM3NetworkPrinting/resources/png/MakerBot Method X.png b/plugins/UM3NetworkPrinting/resources/png/MakerBot Method X.png new file mode 100644 index 0000000000..fc289fe62c Binary files /dev/null and b/plugins/UM3NetworkPrinting/resources/png/MakerBot Method X.png differ diff --git a/plugins/UM3NetworkPrinting/resources/png/MakerBot Method XL.png b/plugins/UM3NetworkPrinting/resources/png/MakerBot Method XL.png new file mode 100644 index 0000000000..c23767459c Binary files /dev/null and b/plugins/UM3NetworkPrinting/resources/png/MakerBot Method XL.png differ diff --git a/plugins/UM3NetworkPrinting/resources/png/MakerBot Method.png b/plugins/UM3NetworkPrinting/resources/png/MakerBot Method.png new file mode 100644 index 0000000000..c48fe68492 Binary files /dev/null and b/plugins/UM3NetworkPrinting/resources/png/MakerBot Method.png differ diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml index 5baae741ac..345bab0ba4 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml @@ -115,7 +115,7 @@ UM.Dialog // Utils function formatPrintJobName(name) { - var extensions = [ ".gcode.gz", ".gz", ".gcode", ".ufp" ] + var extensions = [ ".gcode.gz", ".gz", ".gcode", ".ufp", ".makerbot" ] for (var i = 0; i < extensions.length; i++) { var extension = extensions[i] diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 318fceeb40..e5524a2e45 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -82,13 +82,22 @@ class CloudApiClient: # HACK: There is something weird going on with the API, as it reports printer types in formats like # "ultimaker_s3", but wants "Ultimaker S3" when using the machine_variant filter query. So we need to do some # conversion! + # API points to "MakerBot Method" for a makerbot printertypes which we already changed to allign with other printer_type - machine_type = machine_type.replace("_plus", "+") - machine_type = machine_type.replace("_", " ") - machine_type = machine_type.replace("ultimaker", "ultimaker ") - machine_type = machine_type.replace(" ", " ") - machine_type = machine_type.title() - machine_type = urllib.parse.quote_plus(machine_type) + method_x = { + "ultimaker_method":"MakerBot Method", + "ultimaker_methodx":"MakerBot Method X", + "ultimaker_methodxl":"MakerBot Method XL" + } + if machine_type in method_x: + machine_type = method_x[machine_type] + else: + machine_type = machine_type.replace("_plus", "+") + machine_type = machine_type.replace("_", " ") + machine_type = machine_type.replace("ultimaker", "ultimaker ") + machine_type = machine_type.replace(" ", " ") + machine_type = machine_type.title() + machine_type = urllib.parse.quote_plus(machine_type) url = f"{self.CLUSTER_API_ROOT}/clusters?machine_variant={machine_type}" self._http.get(url, scope=self._scope, diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index c5c144d273..edbc509d84 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -58,6 +58,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # The minimum version of firmware that support print job actions over cloud. PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.2.12") + PRINT_JOB_ACTIONS_MIN_VERSION_METHOD = Version("2.700") # 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. @@ -325,8 +326,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): if not self._printers: return False version_number = self.printers[0].firmwareVersion.split(".") - firmware_version = Version([version_number[0], version_number[1], version_number[2]]) - return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION + if len(version_number)> 2: + firmware_version = Version([version_number[0], version_number[1], version_number[2]]) + return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION + else: + firmware_version = Version([version_number[0], version_number[1]]) + return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION_METHOD + @pyqtProperty(bool, constant = True) def supportsPrintJobQueue(self) -> bool: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 5ec0db8a66..6fbbf2f053 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -9,6 +9,7 @@ from PyQt6.QtWidgets import QMessageBox from UM import i18nCatalog from UM.Logger import Logger # To log errors talking to the API. +from UM.Message import Message from UM.Settings.Interfaces import ContainerInterface from UM.Signal import Signal from UM.Util import parseBool @@ -25,7 +26,7 @@ from .CloudOutputDevice import CloudOutputDevice from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Messages.NewPrinterDetectedMessage import NewPrinterDetectedMessage - +catalog = i18nCatalog("cura") class CloudOutputDeviceManager: """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. @@ -179,6 +180,13 @@ class CloudOutputDeviceManager: return Logger.log("e", f"Failed writing to specific cloud printer: {unique_id} not in remote clusters.") + # This message is added so that user knows when the print job was not sent to cloud printer + message = Message(catalog.i18nc("@info:status", + "Failed writing to specific cloud printer: {0} not in remote clusters.").format(unique_id), + title=catalog.i18nc("@info:title", "Error"), + message_type=Message.MessageType.ERROR) + message.show() + def _createMachineStacksForDiscoveredClusters(self, discovered_clusters: List[CloudClusterResponse]) -> None: """**Synchronously** create machines for discovered devices diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 7fc1b4a7d3..e6054773d8 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -106,6 +106,10 @@ class MeshFormatHandler: 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 makerbot firmware version >=2.700: makerbot is supported + elif "application/x-makerbot" not in machine_file_formats and Version(firmware_version >= Version("2.700")): + machine_file_formats = ["application/x-makerbot"] + 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/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index c8f3be282e..713582b8ad 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional, List +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice from ..BaseModel import BaseModel @@ -34,7 +35,7 @@ class CloudClusterResponse(BaseModel): self.host_version = host_version self.host_internal_ip = host_internal_ip self.friendly_name = friendly_name - self.printer_type = printer_type + self.printer_type = NetworkedPrinterOutputDevice.applyPrinterTypeMapping(printer_type) self.printer_count = printer_count self.capabilities = capabilities if capabilities is not None else [] super().__init__(**kwargs) @@ -51,3 +52,4 @@ class CloudClusterResponse(BaseModel): :return: A human-readable representation of the data in this object. """ return str({k: v for k, v in self.__dict__.items() if k in {"cluster_id", "host_guid", "host_name", "status", "is_online", "host_version", "host_internal_ip", "friendly_name", "printer_type", "printer_count", "capabilities"}}) +