Merge branch 'main' into CURA-11078_python_312_spike

This commit is contained in:
Jelle Spijker 2023-11-09 12:29:25 +01:00 committed by GitHub
commit 052c37ae6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
291 changed files with 76894 additions and 67230 deletions

View file

@ -1259,7 +1259,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
machine_manager.setIntentByCategory(self._intent_category_to_apply)
else:
# if no intent is provided, reset to the default (balanced) intent
machine_manager.resetIntents()
# Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop())

View file

@ -35,10 +35,12 @@ class WorkspaceDialog(QObject):
self._qml_url = "WorkspaceDialog.qml"
self._lock = threading.Lock()
self._default_strategy = None
self._result = {"machine": self._default_strategy,
"quality_changes": self._default_strategy,
"definition_changes": self._default_strategy,
"material": self._default_strategy}
self._result = {
"machine": self._default_strategy,
"quality_changes": self._default_strategy,
"definition_changes": self._default_strategy,
"material": self._default_strategy,
}
self._override_machine = None
self._visible = False
self.showDialogSignal.connect(self.__show)
@ -347,10 +349,12 @@ class WorkspaceDialog(QObject):
if threading.current_thread() != threading.main_thread():
self._lock.acquire()
# Reset the result
self._result = {"machine": self._default_strategy,
"quality_changes": self._default_strategy,
"definition_changes": self._default_strategy,
"material": self._default_strategy}
self._result = {
"machine": self._default_strategy,
"quality_changes": self._default_strategy,
"definition_changes": self._default_strategy,
"material": self._default_strategy,
}
self._visible = True
self.showDialogSignal.emit()

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

View file

@ -0,0 +1,306 @@
# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from io import StringIO, BufferedIOBase
import json
from typing import cast, List, Optional, Dict
from zipfile import BadZipFile, ZipFile, ZIP_DEFLATED
import pyDulcificum as du
from PyQt6.QtCore import QBuffer
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Mesh.MeshWriter import MeshWriter
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from cura.Snapshot import Snapshot
from cura.Utils.Threading import call_on_qt_thread
from cura.CuraVersion import ConanInstalls
catalog = i18nCatalog("cura")
class MakerbotWriter(MeshWriter):
"""A file writer that writes '.makerbot' files."""
def __init__(self) -> None:
super().__init__(add_to_recent_files=False)
Logger.info(f"Using PyDulcificum: {du.__version__}")
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-makerbot",
comment="Makerbot Toolpath Package",
suffixes=["makerbot"]
)
)
_PNG_FORMATS = [
{"prefix": "isometric_thumbnail", "width": 120, "height": 120},
{"prefix": "isometric_thumbnail", "width": 320, "height": 320},
{"prefix": "isometric_thumbnail", "width": 640, "height": 640},
{"prefix": "thumbnail", "width": 140, "height": 106},
{"prefix": "thumbnail", "width": 212, "height": 300},
{"prefix": "thumbnail", "width": 960, "height": 1460},
{"prefix": "thumbnail", "width": 90, "height": 90},
]
_META_VERSION = "3.0.0"
_PRINT_NAME_MAP = {
"UltiMaker Method": "fire_e",
"UltiMaker Method X": "lava_f",
"UltiMaker Method XL": "magma_10",
}
_EXTRUDER_NAME_MAP = {
"1XA": "mk14_hot",
"2XA": "mk14_hot_s",
"1C": "mk14_c",
"1A": "mk14",
"2A": "mk14_s",
}
_MATERIAL_MAP = {"2780b345-577b-4a24-a2c5-12e6aad3e690": "abs",
"88c8919c-6a09-471a-b7b6-e801263d862d": "abs-wss1",
"416eead4-0d8e-4f0b-8bfc-a91a519befa5": "asa",
"85bbae0e-938d-46fb-989f-c9b3689dc4f0": "nylon-cf",
"283d439a-3490-4481-920c-c51d8cdecf9c": "nylon",
"62414577-94d1-490d-b1e4-7ef3ec40db02": "pc",
"69386c85-5b6c-421a-bec5-aeb1fb33f060": "petg",
"0ff92885-617b-4144-a03c-9989872454bc": "pla",
"a4255da2-cb2a-4042-be49-4a83957a2f9a": "pva",
"a140ef8f-4f26-4e73-abe0-cfc29d6d1024": "wss1",
"77873465-83a9-4283-bc44-4e542b8eb3eb": "sr30",
"96fca5d9-0371-4516-9e96-8e8182677f3c": "im-pla",
"9f52c514-bb53-46a6-8c0c-d507cd6ee742": "abs",
"0f9a2a91-f9d6-4b6b-bd9b-a120a29391be": "abs",
"d3e972f2-68c0-4d2f-8cfd-91028dfc3381": "abs",
"495a0ce5-9daf-4a16-b7b2-06856d82394d": "abs-cf10",
"cb76bd6e-91fd-480c-a191-12301712ec77": "abs-wss1",
"a017777e-3f37-4d89-a96c-dc71219aac77": "abs-wss1",
"4d96000d-66de-4d54-a580-91827dcfd28f": "abs-wss1",
"0ecb0e1a-6a66-49fb-b9ea-61a8924e0cf5": "asa",
"efebc2ea-2381-4937-926f-e824524524a5": "asa",
"b0199512-5714-4951-af85-be19693430f8": "asa",
"b9f55a0a-a2b6-4b8d-8d48-07802c575bd1": "pla",
"c439d884-9cdc-4296-a12c-1bacae01003f": "pla",
"16a723e3-44df-49f4-82ec-2a1173c1e7d9": "pla",
"74d0f5c2-fdfd-4c56-baf1-ff5fa92d177e": "pla",
"64dcb783-470d-4400-91b1-7001652f20da": "pla",
"3a1b479b-899c-46eb-a2ea-67050d1a4937": "pla",
"4708ac49-5dde-4cc2-8c0a-87425a92c2b3": "pla",
"4b560eda-1719-407f-b085-1c2c1fc8ffc1": "pla",
"e10a287d-0067-4a58-9083-b7054f479991": "im-pla",
"01a6b5b0-fab1-420c-a5d9-31713cbeb404": "im-pla",
"f65df4ad-a027-4a48-a51d-975cc8b87041": "im-pla",
"f48739f8-6d96-4a3d-9a2e-8505a47e2e35": "im-pla",
"5c7d7672-e885-4452-9a78-8ba90ec79937": "petg",
"91e05a6e-2f5b-4964-b973-d83b5afe6db4": "petg",
"bdc7dd03-bf38-48ee-aeca-c3e11cee799e": "petg",
"54f66c89-998d-4070-aa60-1cb0fd887518": "nylon",
"002c84b3-84ac-4b5a-b57d-fe1f555a6351": "pva",
"e4da5fcb-f62d-48a2-aaef-0b645aa6973b": "wss1",
"77f06146-6569-437d-8380-9edb0d635a32": "sr30"}
# must be called from the main thread because of OpenGL
@staticmethod
@call_on_qt_thread
def _createThumbnail(width: int, height: int) -> Optional[QBuffer]:
if not CuraApplication.getInstance().isVisible:
Logger.warning("Can't create snapshot when renderer not initialized.")
return
try:
snapshot = Snapshot.isometricSnapshot(width, height)
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.OpenModeFlag.WriteOnly)
snapshot.save(thumbnail_buffer, "PNG")
return thumbnail_buffer
except:
Logger.logException("w", "Failed to create snapshot image")
return None
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool:
if mode != MeshWriter.OutputMode.BinaryMode:
Logger.log("e", "MakerbotWriter does not support text mode.")
self.setInformation(catalog.i18nc("@error:not supported", "MakerbotWriter does not support text mode."))
return False
# The GCodeWriter plugin is always available since it is in the "required" list of plugins.
gcode_writer = PluginRegistry.getInstance().getPluginObject("GCodeWriter")
if gcode_writer is None:
Logger.log("e", "Could not find the GCodeWriter plugin, is it disabled?.")
self.setInformation(
catalog.i18nc("@error:load", "Could not load GCodeWriter plugin. Try to re-enable the plugin."))
return False
gcode_writer = cast(MeshWriter, gcode_writer)
gcode_text_io = StringIO()
success = gcode_writer.write(gcode_text_io, None)
# Writing the g-code failed. Then I can also not write the gzipped g-code.
if not success:
self.setInformation(gcode_writer.getInformation())
return False
json_toolpaths = du.gcode_2_miracle_jtp(gcode_text_io.getvalue())
metadata = self._getMeta(nodes)
png_files = []
for png_format in self._PNG_FORMATS:
width, height, prefix = png_format["width"], png_format["height"], png_format["prefix"]
thumbnail_buffer = self._createThumbnail(width, height)
if thumbnail_buffer is None:
Logger.warning(f"Could not create thumbnail of size {width}x{height}.")
continue
png_files.append({
"file": f"{prefix}_{width}x{height}.png",
"data": thumbnail_buffer.data(),
})
try:
with ZipFile(stream, "w", compression=ZIP_DEFLATED) as zip_stream:
zip_stream.writestr("meta.json", json.dumps(metadata, indent=4))
zip_stream.writestr("print.jsontoolpath", json_toolpaths)
for png_file in png_files:
file, data = png_file["file"], png_file["data"]
zip_stream.writestr(file, data)
except (IOError, OSError, BadZipFile) as ex:
Logger.log("e", f"Could not write to (.makerbot) file because: '{ex}'.")
self.setInformation(catalog.i18nc("@error", "MakerbotWriter could not save to the designated path."))
return False
return True
def _getMeta(self, root_nodes: List[SceneNode]) -> Dict[str, any]:
application = CuraApplication.getInstance()
machine_manager = application.getMachineManager()
global_stack = machine_manager.activeMachine
extruders = global_stack.extruderList
nodes = []
for root_node in root_nodes:
for node in DepthFirstIterator(root_node):
if not getattr(node, "_outside_buildarea", False):
if node.callDecoration(
"isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration(
"isNonThumbnailVisibleMesh"):
nodes.append(node)
meta = dict()
meta["bot_type"] = MakerbotWriter._PRINT_NAME_MAP.get((name := global_stack.definition.name), name)
bounds: Optional[AxisAlignedBox] = None
for node in nodes:
node_bounds = node.getBoundingBox()
if node_bounds is None:
continue
if bounds is None:
bounds = node_bounds
else:
bounds = bounds + node_bounds
if bounds is not None:
meta["bounding_box"] = {
"x_min": bounds.left,
"x_max": bounds.right,
"y_min": bounds.back,
"y_max": bounds.front,
"z_min": bounds.bottom,
"z_max": bounds.top,
}
material_bed_temperature = global_stack.getProperty("material_bed_temperature", "value")
meta["platform_temperature"] = material_bed_temperature
build_volume_temperature = global_stack.getProperty("build_volume_temperature", "value")
meta["build_plane_temperature"] = build_volume_temperature
print_information = application.getPrintInformation()
meta["commanded_duration_s"] = int(print_information.currentPrintTime)
meta["duration_s"] = int(print_information.currentPrintTime)
material_lengths = list(map(meterToMillimeter, print_information.materialLengths))
meta["extrusion_distance_mm"] = material_lengths[0]
meta["extrusion_distances_mm"] = material_lengths
meta["extrusion_mass_g"] = print_information.materialWeights[0]
meta["extrusion_masses_g"] = print_information.materialWeights
meta["uuid"] = print_information.slice_uuid
materials = []
for extruder in extruders:
guid = extruder.material.getMetaData().get("GUID")
material_name = extruder.material.getMetaData().get("material")
material = self._MATERIAL_MAP.get(guid, material_name)
materials.append(material)
meta["material"] = materials[0]
meta["materials"] = materials
materials_temps = [extruder.getProperty("default_material_print_temperature", "value") for extruder in
extruders]
meta["extruder_temperature"] = materials_temps[0]
meta["extruder_temperatures"] = materials_temps
meta["model_counts"] = [{"count": 1, "name": node.getName()} for node in nodes]
tool_types = [MakerbotWriter._EXTRUDER_NAME_MAP.get((name := extruder.variant.getName()), name) for extruder in
extruders]
meta["tool_type"] = tool_types[0]
meta["tool_types"] = tool_types
meta["version"] = MakerbotWriter._META_VERSION
meta["preferences"] = dict()
for node in nodes:
bounds = node.getBoundingBox()
meta["preferences"][str(node.getName())] = {
"machineBounds": [bounds.right, bounds.back, bounds.left, bounds.front] if bounds is not None else None,
"printMode": CuraApplication.getInstance().getIntentManager().currentIntentCategory,
}
meta["miracle_config"] = {"gaggles": {str(node.getName()): {} for node in nodes}}
cura_engine_info = ConanInstalls.get("curaengine", {"version": "unknown", "revision": "unknown"})
meta["curaengine_version"] = cura_engine_info["version"]
meta["curaengine_commit_hash"] = cura_engine_info["revision"]
dulcificum_info = ConanInstalls.get("dulcificum", {"version": "unknown", "revision": "unknown"})
meta["dulcificum_version"] = dulcificum_info["version"]
meta["dulcificum_commit_hash"] = dulcificum_info["revision"]
meta["makerbot_writer_version"] = self.getVersion()
meta["pyDulcificum_version"] = du.__version__
# Add engine plugin information to the metadata
for name, package_info in ConanInstalls.items():
if not name.startswith("curaengine_"):
continue
meta[f"{name}_version"] = package_info["version"]
meta[f"{name}_commit_hash"] = package_info["revision"]
# TODO add the following instructions
# num_tool_changes
# num_z_layers
# num_z_transitions
# platform_temperature
# total_commands
return meta
def meterToMillimeter(value: float) -> float:
"""Converts a value in meters to millimeters."""
return value * 1000.0

View file

@ -0,0 +1,28 @@
# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from . import MakerbotWriter
catalog = i18nCatalog("cura")
def getMetaData():
file_extension = "makerbot"
return {
"mesh_writer": {
"output": [{
"extension": file_extension,
"description": catalog.i18nc("@item:inlistbox", "Makerbot Printfile"),
"mime_type": "application/x-makerbot",
"mode": MakerbotWriter.MakerbotWriter.OutputMode.BinaryMode,
}],
}
}
def register(app):
return {
"mesh_writer": MakerbotWriter.MakerbotWriter(),
}

View file

@ -0,0 +1,13 @@
{
"name": "Makerbot Printfile Writer",
"author": "UltiMaker",
"version": "0.1.0",
"description": "Provides support for writing MakerBot Format Packages.",
"api": 8,
"supported_sdk_versions": [
"8.0.0",
"8.1.0",
"8.2.0"
],
"i18n-catalog": "cura"
}

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

@ -1,4 +1,4 @@
// Copyright (c) 2019 Ultimaker B.V.
// Copyright (c) 2023 UltiMaker
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
@ -6,7 +6,7 @@ import UM 1.3 as UM
import Cura 1.0 as Cura
Item {
property var cameraUrl: "";
property string cameraUrl: "";
Rectangle {
anchors.fill:parent;
@ -34,22 +34,29 @@ Item {
Cura.NetworkMJPGImage {
id: cameraImage
anchors.horizontalCenter: parent.horizontalCenter;
anchors.verticalCenter: parent.verticalCenter;
height: Math.round((imageHeight === 0 ? 600 * screenScaleFactor : imageHeight) * width / imageWidth);
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
readonly property real img_scale_factor: {
if (imageWidth > maximumWidth || imageHeight > maximumHeight) {
return Math.min(maximumWidth / imageWidth, maximumHeight / imageHeight);
}
return 1.0;
}
width: imageWidth === 0 ? 800 * screenScaleFactor : imageWidth * img_scale_factor
height: imageHeight === 0 ? 600 * screenScaleFactor : imageHeight * img_scale_factor
onVisibleChanged: {
if (cameraUrl === "") return;
if (visible) {
if (cameraUrl != "") {
start();
}
start();
} else {
if (cameraUrl != "") {
stop();
}
stop();
}
}
source: cameraUrl
width: Math.min(imageWidth === 0 ? 800 * screenScaleFactor : imageWidth, maximumWidth);
z: 1
}

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

View file

@ -11,7 +11,7 @@ import re
class VersionUpgrade54to55(VersionUpgrade):
profile_regex = re.compile(
r"um\_(?P<machine>s(3|5|7))_(?P<core_type>aa|cc|bb)(?P<nozzle_size>0\.(6|4|8))_(?P<material>pla|petg|abs|cpe|cpe_plus|nylon|pc|petcf|tough_pla|tpu)_(?P<layer_height>0\.\d{1,2}mm)")
r"um\_(?P<machine>s(3|5|7))_(?P<core_type>aa|cc|bb)(?P<nozzle_size>0\.(6|4|8))_(?P<material>pla|petg|abs|tough_pla)_(?P<layer_height>0\.\d{1,2}mm)")
@staticmethod
def _isUpgradedUltimakerDefinitionId(definition_id: str) -> bool: