Merge pull request #17149 from Ultimaker/CURA-11138-makerbot-cloud-printing

CURA-11138-makerbot-cloud-printing
This commit is contained in:
Remco Burema 2023-11-01 13:42:35 +01:00 committed by GitHub
commit 82d0bf4673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 148 additions and 28 deletions

View File

@ -51,6 +51,9 @@ class CompatibleMachineModel(ListModel):
for output_device in machine_manager.printerOutputDevices: for output_device in machine_manager.printerOutputDevices:
for printer in output_device.printers: for printer in output_device.printers:
extruder_configs = dict() 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: # initialize & add current active material:
for extruder in printer.extruders: for extruder in printer.extruders:

View File

@ -40,9 +40,22 @@ class ExtruderConfigurationModel(QObject):
def setHotendID(self, hotend_id: Optional[str]) -> None: def setHotendID(self, hotend_id: Optional[str]) -> None:
if self._hotend_id != hotend_id: if self._hotend_id != hotend_id:
self._hotend_id = hotend_id self._hotend_id = ExtruderConfigurationModel.applyNameMappingHotend(hotend_id)
self.extruderConfigurationChanged.emit() 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) @pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
def hotendID(self) -> Optional[str]: def hotendID(self) -> Optional[str]:
return self._hotend_id return self._hotend_id

View File

@ -9,7 +9,9 @@ from PyQt6.QtCore import pyqtProperty, QObject
class MaterialOutputModel(QObject): class MaterialOutputModel(QObject):
def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None: def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._guid = guid
name, guid = MaterialOutputModel.getMaterialFromDefinition(guid,type, brand, name)
self._guid =guid
self._type = type self._type = type
self._color = color self._color = color
self._brand = brand self._brand = brand
@ -19,6 +21,34 @@ class MaterialOutputModel(QObject):
def guid(self) -> str: def guid(self) -> str:
return self._guid if self._guid else "" 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) @pyqtProperty(str, constant = True)
def type(self) -> str: def type(self) -> str:
return self._type return self._type

View File

@ -415,7 +415,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def printerType(self) -> str: 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) @pyqtProperty(str, constant = True)
def ipAddress(self) -> str: def ipAddress(self) -> str:

View File

@ -284,16 +284,20 @@ class CuraStackBuilder:
abstract_machines = registry.findContainerStacks(id = abstract_machine_id) abstract_machines = registry.findContainerStacks(id = abstract_machine_id)
if abstract_machines: if abstract_machines:
return cast(GlobalStack, abstract_machines[0]) return cast(GlobalStack, abstract_machines[0])
definitions = registry.findDefinitionContainers(id=definition_id) definitions = registry.findDefinitionContainers(id=definition_id)
name = "" name = ""
if definitions: if definitions:
name = definitions[0].getName() name = definitions[0].getName()
stack = cls.createMachine(abstract_machine_id, definition_id, show_warning_message=False) stack = cls.createMachine(abstract_machine_id, definition_id, show_warning_message=False)
if not stack: if not stack:
return None return None
if not stack.getMetaDataEntry("visible", True):
return None
stack.setName(name) stack.setName(name)
stack.setMetaDataEntry("is_abstract_machine", True) stack.setMetaDataEntry("is_abstract_machine", True)

View File

@ -208,12 +208,14 @@ Item
anchors.rightMargin: UM.Theme.getSize("thin_margin").height anchors.rightMargin: UM.Theme.getSize("thin_margin").height
enabled: UM.Backend.state == UM.Backend.Done 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" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ 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"] }, { text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] },
] ]
} }

View File

@ -27,7 +27,7 @@ from .ExportFileJob import ExportFileJob
class DFFileExportAndUploadManager: class DFFileExportAndUploadManager:
""" """
Class responsible for exporting the scene and uploading the exported data to the Digital Factory Library. Since 3mf 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. both files and updates those messages according to the progress of both the file job uploads.
""" """
def __init__(self, file_handlers: Dict[str, FileHandler], def __init__(self, file_handlers: Dict[str, FileHandler],
@ -118,7 +118,7 @@ class DFFileExportAndUploadManager:
library_project_id = self._library_project_id, library_project_id = self._library_project_id,
source_file_id = self._source_file_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: 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 """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. 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() reply_string = bytes(reply.readAll()).decode()
filename_ufp = self._file_name + ".ufp" if "ufp" in self._formats:
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)) 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: with self._message_lock:
# Set the progress to 100% when the upload job fails, to avoid having the progress message stuck # 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_meshfile]["upload_status"] = "failed"
self._file_upload_job_metadata[filename_ufp]["upload_progress"] = 100 self._file_upload_job_metadata[filename_meshfile]["upload_progress"] = 100
human_readable_error = self.extractErrorTitle(reply_string) 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", 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", message_type_str = "ERROR",
lifetime = 30 lifetime = 30
) )
self._on_upload_error() self._on_upload_error()
self._onFileUploadFinished(filename_ufp) self._onFileUploadFinished(filename_meshfile)
@staticmethod @staticmethod
def extractErrorTitle(reply_body: Optional[str]) -> str: 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 = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp")
job_ufp.finished.connect(self._onPrintFileExported) job_ufp.finished.connect(self._onPrintFileExported)
self._upload_jobs.append(job_ufp) 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 return metadata

View File

@ -313,7 +313,7 @@ class DigitalFactoryApiClient:
error_callback = on_error, error_callback = on_error,
timeout = self.DEFAULT_REQUEST_TIMEOUT) timeout = self.DEFAULT_REQUEST_TIMEOUT)
def requestUploadUFP(self, request: DFPrintJobUploadRequest, def requestUploadMeshFile(self, request: DFPrintJobUploadRequest,
on_finished: Callable[[DFPrintJobUploadResponse], Any], on_finished: Callable[[DFPrintJobUploadResponse], Any],
on_error: Optional[Callable[["QNetworkReply", "QNetworkReply.NetworkError"], None]] = None) -> None: 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. """Requests the Digital Factory to register the upload of a file in a library project.

View File

@ -92,7 +92,8 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
if not self._controller.file_handlers: if not self._controller.file_handlers:
self._controller.file_handlers = { self._controller.file_handlers = {
"3mf": CuraApplication.getInstance().getWorkspaceFileHandler(), "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}) self._dialog = CuraApplication.getInstance().createQmlComponent(self._dialog_path, {"manager": self._controller})

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@ -115,7 +115,7 @@ UM.Dialog
// Utils // Utils
function formatPrintJobName(name) 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++) for (var i = 0; i < extensions.length; i++)
{ {
var extension = extensions[i] var extension = extensions[i]

View File

@ -82,7 +82,16 @@ class CloudApiClient:
# HACK: There is something weird going on with the API, as it reports printer types in formats like # 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 # "ultimaker_s3", but wants "Ultimaker S3" when using the machine_variant filter query. So we need to do some
# conversion! # conversion!
# API points to "MakerBot Method" for a makerbot printertypes which we already changed to allign with other printer_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("_plus", "+")
machine_type = machine_type.replace("_", " ") machine_type = machine_type.replace("_", " ")
machine_type = machine_type.replace("ultimaker", "ultimaker ") machine_type = machine_type.replace("ultimaker", "ultimaker ")

View File

@ -58,6 +58,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# The minimum version of firmware that support print job actions over cloud. # 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 = 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. # 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. # Therefore, we create a private signal used to trigger the printersChanged signal.
@ -325,8 +326,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
if not self._printers: if not self._printers:
return False return False
version_number = self.printers[0].firmwareVersion.split(".") version_number = self.printers[0].firmwareVersion.split(".")
if len(version_number)> 2:
firmware_version = Version([version_number[0], version_number[1], version_number[2]]) firmware_version = Version([version_number[0], version_number[1], version_number[2]])
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION 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) @pyqtProperty(bool, constant = True)
def supportsPrintJobQueue(self) -> bool: def supportsPrintJobQueue(self) -> bool:

View File

@ -9,6 +9,7 @@ from PyQt6.QtWidgets import QMessageBox
from UM import i18nCatalog from UM import i18nCatalog
from UM.Logger import Logger # To log errors talking to the API. 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.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal from UM.Signal import Signal
from UM.Util import parseBool from UM.Util import parseBool
@ -25,7 +26,7 @@ from .CloudOutputDevice import CloudOutputDevice
from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage
from ..Models.Http.CloudClusterResponse import CloudClusterResponse from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Messages.NewPrinterDetectedMessage import NewPrinterDetectedMessage from ..Messages.NewPrinterDetectedMessage import NewPrinterDetectedMessage
catalog = i18nCatalog("cura")
class CloudOutputDeviceManager: class CloudOutputDeviceManager:
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
@ -179,6 +180,13 @@ class CloudOutputDeviceManager:
return return
Logger.log("e", f"Failed writing to specific cloud printer: {unique_id} not in remote clusters.") 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: def _createMachineStacksForDiscoveredClusters(self, discovered_clusters: List[CloudClusterResponse]) -> None:
"""**Synchronously** create machines for discovered devices """**Synchronously** create machines for discovered devices

View File

@ -106,6 +106,10 @@ class MeshFormatHandler:
if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"): 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 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. # Take the intersection between file_formats and machine_file_formats.
format_by_mimetype = {f["mime_type"]: f for f in file_formats} format_by_mimetype = {f["mime_type"]: f for f in file_formats}

View File

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, List from typing import Optional, List
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
@ -34,7 +35,7 @@ class CloudClusterResponse(BaseModel):
self.host_version = host_version self.host_version = host_version
self.host_internal_ip = host_internal_ip self.host_internal_ip = host_internal_ip
self.friendly_name = friendly_name self.friendly_name = friendly_name
self.printer_type = printer_type self.printer_type = NetworkedPrinterOutputDevice.applyPrinterTypeMapping(printer_type)
self.printer_count = printer_count self.printer_count = printer_count
self.capabilities = capabilities if capabilities is not None else [] self.capabilities = capabilities if capabilities is not None else []
super().__init__(**kwargs) super().__init__(**kwargs)
@ -51,3 +52,4 @@ class CloudClusterResponse(BaseModel):
:return: A human-readable representation of the data in this object. :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"}}) 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"}})