diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index 98e4f67f82..56577b5655 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -102,6 +102,13 @@ class MaterialManager(QObject): # GUID -> material group list self._guid_material_groups_map = defaultdict(list) for root_material_id, material_group in self._material_group_map.items(): + # This can happen when we are updating with incomplete data. + if material_group.root_material_node is None: + Logger.log("e", "Missing root material node for [%s]. Probably caused by update using incomplete data." + " Check all related signals for further debugging.", + material_group.name) + # Do nothing here, we wait for a next signal to trigger an update. + return guid = material_group.root_material_node.metadata["GUID"] self._guid_material_groups_map[guid].append(material_group) diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index 5c09a4f060..0a1337feeb 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -40,8 +40,6 @@ class BaseMaterialsModel(ListModel): self._extruder_position = 0 self._extruder_stack = None - self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) - def _updateExtruderStack(self): global_stack = self._machine_manager.activeMachine if global_stack is None: diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index cbe882f253..6b56ec89f5 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -6,6 +6,7 @@ from UM.Application import Application from UM.Logger import Logger from UM.i18n import i18nCatalog +from UM.Signal import postponeSignals, CompressTechnique from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer @@ -434,6 +435,24 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # \param file_name @call_on_qt_thread def read(self, file_name): + container_registry = ContainerRegistry.getInstance() + signals = [container_registry.containerAdded, + container_registry.containerRemoved, + container_registry.containerMetaDataChanged] + # + # We now have different managers updating their lookup tables upon container changes. It is critical to make + # sure that the managers have a complete set of data when they update. + # + # In project loading, lots of the container-related signals are loosely emitted, which can create timing gaps + # for incomplete data update or other kinds of issues to happen. + # + # To avoid this, we postpone all signals so they don't get emitted immediately. But, please also be aware that, + # because of this, do not expect to have the latest data in the lookup tables in project loading. + # + with postponeSignals(*signals, compress = CompressTechnique.CompressSingle): + return self._read(file_name) + + def _read(self, file_name): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 507274d355..3f5e69317e 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -1,14 +1,15 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Workspace.WorkspaceWriter import WorkspaceWriter +import configparser +from io import StringIO +import zipfile + from UM.Application import Application +from UM.Logger import Logger from UM.Preferences import Preferences from UM.Settings.ContainerRegistry import ContainerRegistry -from cura.Settings.ExtruderManager import ExtruderManager -import zipfile -from io import StringIO -import configparser +from UM.Workspace.WorkspaceWriter import WorkspaceWriter class ThreeMFWorkspaceWriter(WorkspaceWriter): @@ -16,7 +17,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): super().__init__() def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): - mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter") + application = Application.getInstance() + machine_manager = application.getMachineManager() + + mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter") if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace return False @@ -29,17 +33,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if archive is None: # This happens if there was no mesh data to write. archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_stack = machine_manager.activeMachine # Add global container stack data to the archive. - self._writeContainerToArchive(global_container_stack, archive) + self._writeContainerToArchive(global_stack, archive) # Also write all containers in the stack to the file - for container in global_container_stack.getContainers(): + for container in global_stack.getContainers(): self._writeContainerToArchive(container, archive) # Check if the machine has extruders and save all that data as well. - for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()): + for extruder_stack in global_stack.extruders.values(): self._writeContainerToArchive(extruder_stack, archive) for container in extruder_stack.getContainers(): self._writeContainerToArchive(container, archive) @@ -59,9 +63,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): version_file = zipfile.ZipInfo("Cura/version.ini") version_config_parser = configparser.ConfigParser(interpolation = None) version_config_parser.add_section("versions") - version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion()) - version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType()) - version_config_parser.set("versions", "is_debug_mode", str(Application.getInstance().getIsDebugMode())) + version_config_parser.set("versions", "cura_version", application.getVersion()) + version_config_parser.set("versions", "build_type", application.getBuildType()) + version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode())) version_file_string = StringIO() version_config_parser.write(version_file_string) @@ -85,7 +89,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): # Some containers have a base file, which should then be the file to use. if "base_file" in container.getMetaData(): base_file = container.getMetaDataEntry("base_file") - container = ContainerRegistry.getInstance().findContainers(id = base_file)[0] + if base_file != container.getId(): + container = ContainerRegistry.getInstance().findContainers(id = base_file)[0] file_name = "Cura/%s.%s" % (container.getId(), file_suffix) diff --git a/plugins/PostProcessingPlugin/scripts/ColorChange.py b/plugins/PostProcessingPlugin/scripts/FilamentChange.py similarity index 79% rename from plugins/PostProcessingPlugin/scripts/ColorChange.py rename to plugins/PostProcessingPlugin/scripts/FilamentChange.py index 8db45f4033..2bb7891634 100644 --- a/plugins/PostProcessingPlugin/scripts/ColorChange.py +++ b/plugins/PostProcessingPlugin/scripts/FilamentChange.py @@ -2,17 +2,15 @@ # under the terms of the AGPLv3 or higher from ..Script import Script -#from UM.Logger import Logger -# from cura.Settings.ExtruderManager import ExtruderManager -class ColorChange(Script): +class FilamentChange(Script): def __init__(self): super().__init__() def getSettingDataString(self): return """{ - "name":"Color Change", - "key": "ColorChange", + "name":"Filament Change", + "key": "FilamentChange", "metadata": {}, "version": 2, "settings": @@ -60,17 +58,17 @@ class ColorChange(Script): if later_retract is not None and later_retract > 0.: color_change = color_change + (" L%.2f" % later_retract) - color_change = color_change + " ; Generated by ColorChange plugin" + color_change = color_change + " ; Generated by FilamentChange plugin" - layer_targets = layer_nums.split(',') + layer_targets = layer_nums.split(",") if len(layer_targets) > 0: for layer_num in layer_targets: - layer_num = int( layer_num.strip() ) + layer_num = int(layer_num.strip()) if layer_num < len(data): - layer = data[ layer_num - 1 ] + layer = data[layer_num - 1] lines = layer.split("\n") - lines.insert(2, color_change ) - final_line = "\n".join( lines ) - data[ layer_num - 1 ] = final_line + lines.insert(2, color_change) + final_line = "\n".join(lines) + data[layer_num - 1] = final_line return data diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 73acc02cae..7a4c590acc 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -93,13 +93,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._gcode = gcode_list + is_job_sent = True if len(self._printers) > 1: self._spawnPrinterSelectionDialog() else: - self.sendPrintJob() + is_job_sent = self.sendPrintJob() # Notify the UI that a switch to the print monitor should happen - Application.getInstance().getController().setActiveStage("MonitorStage") + if is_job_sent: + Application.getInstance().getController().setActiveStage("MonitorStage") def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: @@ -121,7 +123,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() - return + return False self._sending_gcode = True @@ -134,7 +136,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. - return + return False parts = [] @@ -152,6 +154,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress) + return True + @pyqtProperty(QObject, notify=activePrinterChanged) def activePrinter(self) -> Optional["PrinterOutputModel"]: return self._active_printer diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 6e2b5153db..11cc7bf472 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Logger import Logger @@ -17,7 +17,7 @@ from .avr_isp import stk500v2, intelHex from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty -from serial import Serial, SerialException +from serial import Serial, SerialException, SerialTimeoutException from threading import Thread from time import time, sleep from queue import Queue @@ -266,8 +266,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): command = (command + "\n").encode() if not command.endswith(b"\n"): command += b"\n" - self._serial.write(b"\n") - self._serial.write(command) + try: + self._serial.write(b"\n") + self._serial.write(command) + except SerialTimeoutException: + Logger.log("w", "Timeout when sending command to printer via USB.") def _update(self): while self._connection_state == ConnectionState.connected and self._serial is not None: diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index edc782cc9e..a7ba423153 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -200,18 +200,25 @@ class XmlMaterialProfile(InstanceContainer): ## Begin Settings Block builder.start("settings") - if self.getDefinition().getId() == "fdmprinter": + if self.getMetaDataEntry("definition") == "fdmprinter": for instance in self.findInstances(): self._addSettingElement(builder, instance) machine_container_map = {} machine_nozzle_map = {} - variant_manager = CuraApplication.getInstance()._variant_manager + variant_manager = CuraApplication.getInstance().getVariantManager() + material_manager = CuraApplication.getInstance().getMaterialManager() + + root_material_id = self.getMetaDataEntry("base_file") # if basefile is self.getId, this is a basefile. + material_group = material_manager.getMaterialGroup(root_material_id) + + all_containers = [] + for node in [material_group.root_material_node] + material_group.derived_material_node_list: + all_containers.append(node.getContainer()) - all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self.getId()) for container in all_containers: - definition_id = container.getDefinition().getId() + definition_id = container.getMetaDataEntry("definition") if definition_id == "fdmprinter": continue @@ -233,7 +240,8 @@ class XmlMaterialProfile(InstanceContainer): product_id_map = self.getProductIdMap() for definition_id, container in machine_container_map.items(): - definition = container.getDefinition() + definition_id = container.getMetaDataEntry("definition") + definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = definition_id)[0] product = definition_id for product_name, product_id_list in product_id_map.items(): @@ -243,13 +251,14 @@ class XmlMaterialProfile(InstanceContainer): builder.start("machine") builder.start("machine_identifier", { - "manufacturer": container.getMetaDataEntry("machine_manufacturer", definition.getMetaDataEntry("manufacturer", "Unknown")), + "manufacturer": container.getMetaDataEntry("machine_manufacturer", + definition_metadata.get("manufacturer", "Unknown")), "product": product }) builder.end("machine_identifier") for instance in container.findInstances(): - if self.getDefinition().getId() == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value: + if self.getMetaDataEntry("definition") == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value: # If the settings match that of the base profile, just skip since we inherit the base profile. continue