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 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:

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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"] },
]
}

View file

@ -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

View file

@ -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.

View file

@ -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})

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
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]

View file

@ -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,

View file

@ -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:

View file

@ -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

View file

@ -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}

View file

@ -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"}})