From 2392bd795b4e02a70f0aaab5fb7e98afc3f85015 Mon Sep 17 00:00:00 2001 From: Mariska <40423138+MariMakes@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:32:49 +0200 Subject: [PATCH 01/12] Update bugreport.yaml --- .github/ISSUE_TEMPLATE/bugreport.yaml | 59 ++++++++++++--------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yaml b/.github/ISSUE_TEMPLATE/bugreport.yaml index 35316a2d0b..e1ba3264bc 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yaml +++ b/.github/ISSUE_TEMPLATE/bugreport.yaml @@ -5,17 +5,17 @@ body: - type: markdown attributes: value: | - **Thank you for using Cura and wanting to report a bug.** + **Thank you for using Cura and wanting to report a bug. 🙏** - Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. + Before filing, [please check if the issue already exists](https://github.com/Ultimaker/Cura/issues?q=is%3Aissue) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment. - Also, please note the application version in the title of the issue "For example (5.3.1) Cannot connect to 3rd-party printer". Please do not write things like **Request** or **BUG** in the title, this is what labels are for. + Please include the cura version in the title of the issue. For example, *"[5.4.0] Support Brim is missing in this model"*. - type: input attributes: - label: Application Version + label: Cura Version description: The version of Cura this issue occurs with. - placeholder: 5.3.0 + placeholder: 5.4.0 validations: required: true - type: input @@ -28,14 +28,14 @@ body: - type: input attributes: label: Printer - description: Which printer was selected in Cura? - placeholder: Ultimaker S7 + description: Which printer was selected in Cura? It also helps to mention if you made any firmware modifications to your printer. + placeholder: Ultimaker S7 / Creality CR-10 with Klipper validations: required: true - type: textarea attributes: label: Reproduction steps - description: Tell us what you did! + description: Share what you did, so we can reproduce it placeholder: | 1. Something you did 2. Something you did next @@ -44,42 +44,37 @@ body: - type: textarea attributes: label: Actual results - description: What happens after the above steps have been followed. + description: What happens after the above steps have been followed? validations: required: true - type: textarea attributes: label: Expected results - description: What should happen after the above steps have been followed. + description: What should happen after the above steps have been followed? validations: required: true - type: markdown attributes: value: | - Please be sure to add the following files: - * To save a project file go to File -> Save project. - Please make sure to .zip your project file. - For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. - G-code files are not project files! - Before you share, please think to yourself. Is this a model that can be shared? - * **Screenshots** of showing the problem, perhaps before/after images. - * A **log file** for crashes and similar issues. - You can find your log file here: - Windows: `%APPDATA%\cura\\cura.log` or usually `C:\Users\\\AppData\Roaming\cura\\cura.log` - MacOS: `$USER/Library/Application Support/cura//cura.log` - Ubuntu/Linux: `$USER/.local/share/cura//cura.log` - - If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder -- type: checkboxes - attributes: - label: Checklist of files to include - options: - - label: Log file - - label: Project file + ### Please add the following files if it's 🔵 applicable: + * **A Project File** contains the printer and settings we need for troubleshooting. Especially for issues with *🔵 the quality of your print.* + To save a project file go to File -> Save project. + Please make sure to .zip your project file. For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. + G-code files are not project files! Before you share, please think to yourself. Is this a model that can be shared? + ![Alt Text](https://user-images.githubusercontent.com/40423138/240616958-5a9751f2-bd34-4808-9752-6fde2e27516e.gif) + * **Screenshots** showing the issue. Especially for issues when *🔵 using Cura* + * A **log file** with information on what your Cura is doing. Especially for issues with *🔵 unexpected crashes.* + You can find your log file here: + Windows: `%APPDATA%\cura\\cura.log` + MacOS: `$USER/Library/Application Support/cura//cura.log` + Ubuntu/Linux: `$USER/.local/share/cura//cura.log` + If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder + - type: textarea attributes: - label: Additional information & file uploads - description: You can add these files and additional information that is relevant to the issue in the comments below. + label: Add your .zip and screenshots here ⬇️ + description: You can add the zip file and additional information that is relevant to the issue in the comments below. validations: required: true + From a24a93411fcbc1dd363517382d6c0b8a58715adf Mon Sep 17 00:00:00 2001 From: Mariska <40423138+MariMakes@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:35:39 +0200 Subject: [PATCH 02/12] Update bugreport.yaml --- .github/ISSUE_TEMPLATE/bugreport.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yaml b/.github/ISSUE_TEMPLATE/bugreport.yaml index e1ba3264bc..c7269e89a3 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yaml +++ b/.github/ISSUE_TEMPLATE/bugreport.yaml @@ -57,13 +57,16 @@ body: attributes: value: | ### Please add the following files if it's 🔵 applicable: - * **A Project File** contains the printer and settings we need for troubleshooting. Especially for issues with *🔵 the quality of your print.* + 🔵 If you have issues with *the quality of your print.* + Please add **a Project File**, It contains the printer and settings we need for troubleshooting. To save a project file go to File -> Save project. Please make sure to .zip your project file. For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. G-code files are not project files! Before you share, please think to yourself. Is this a model that can be shared? ![Alt Text](https://user-images.githubusercontent.com/40423138/240616958-5a9751f2-bd34-4808-9752-6fde2e27516e.gif) - * **Screenshots** showing the issue. Especially for issues when *🔵 using Cura* - * A **log file** with information on what your Cura is doing. Especially for issues with *🔵 unexpected crashes.* + 🔵 If you have issues with *interacting with Cura* + Please add **Screenshots** showing the issue. + 🔵 Especially for issues with *unexpected crashes and behavior.* + Please add a **log file** with information on what your Cura is doing. You can find your log file here: Windows: `%APPDATA%\cura\\cura.log` MacOS: `$USER/Library/Application Support/cura//cura.log` From 1a4a3c27b98294d1cb29c3829041dd06bc7df0d7 Mon Sep 17 00:00:00 2001 From: Mariska <40423138+MariMakes@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:47:56 +0200 Subject: [PATCH 03/12] Update Bug Template Update Wording Add look-up issue link Update instructions on how to add documents. --- .github/ISSUE_TEMPLATE/bugreport.yaml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yaml b/.github/ISSUE_TEMPLATE/bugreport.yaml index c7269e89a3..18ce1e63e4 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yaml +++ b/.github/ISSUE_TEMPLATE/bugreport.yaml @@ -56,17 +56,18 @@ body: - type: markdown attributes: value: | - ### Please add the following files if it's 🔵 applicable: - 🔵 If you have issues with *the quality of your print.* - Please add **a Project File**, It contains the printer and settings we need for troubleshooting. + ### Please add the following files when they are related to... + * 🔵 **The quality of your print** + Please add **a Project File**. It contains the printer and settings we need for troubleshooting. To save a project file go to File -> Save project. Please make sure to .zip your project file. For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. G-code files are not project files! Before you share, please think to yourself. Is this a model that can be shared? ![Alt Text](https://user-images.githubusercontent.com/40423138/240616958-5a9751f2-bd34-4808-9752-6fde2e27516e.gif) - 🔵 If you have issues with *interacting with Cura* - Please add **Screenshots** showing the issue. - 🔵 Especially for issues with *unexpected crashes and behavior.* - Please add a **log file** with information on what your Cura is doing. + * 🔵 **Using and interacting with Cura** + Please add **screenshots** showing the issue. + Before and after, and arrows can help here. + * 🔵 **Unexpected crashes and behavior** + Please add **a log file** with information on what your Cura is doing. You can find your log file here: Windows: `%APPDATA%\cura\\cura.log` MacOS: `$USER/Library/Application Support/cura//cura.log` @@ -79,5 +80,3 @@ body: description: You can add the zip file and additional information that is relevant to the issue in the comments below. validations: required: true - - From 8fd3ef522128092b59716537270c7a3822a69308 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 6 Jul 2023 16:35:15 +0200 Subject: [PATCH 04/12] Update update-translation.yml Only run on release branches --- .github/workflows/update-translation.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/update-translation.yml b/.github/workflows/update-translation.yml index c1f0e027b8..65693be937 100644 --- a/.github/workflows/update-translation.yml +++ b/.github/workflows/update-translation.yml @@ -17,6 +17,13 @@ on: - 'conandata.yml' - 'GitVersion.yml' - '*.jinja' + branches: + - '[1-9].[0-9]' + - '[1-9].[0-9][0-9]' + tags: + - '[1-9].[0-9].[0-9]*' + - '[1-9].[0-9].[0-9]' + - '[1-9].[0-9][0-9].[0-9]*' jobs: update-translations: From 26bf00ccc05f9c54ddd24f5fc952299a450c948f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 10 Jul 2023 13:25:29 +0200 Subject: [PATCH 05/12] Add backend_plugin messages to proto file CURA-10717 --- plugins/CuraEngineBackend/Cura.proto | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index b420a6ecc2..ef12ab6d6f 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -8,12 +8,26 @@ message ObjectList repeated Setting settings = 2; // meshgroup settings (for one-at-a-time printing) } +enum SlotID { + SIMPLIFY = 0; + POSTPROCESS = 1; +} + +message EnginePlugin +{ + SlotID id = 1; + optional string address = 2; + optional uint32 port = 3; +} + message Slice { repeated ObjectList object_lists = 1; // The meshgroups to be printed one after another SettingList global_settings = 2; // The global settings used for the whole print job repeated Extruder extruders = 3; // The settings sent to each extruder object repeated SettingExtruder limit_to_extruder = 4; // From which stack the setting would inherit if not defined per object + + repeated EnginePlugin engine_plugins = 5; } message Extruder From f3bc7bf28a60ce7555e59d633983e55233fdc141 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 11:24:28 +0200 Subject: [PATCH 06/12] Move backend plugin logic to Cura from Uranium I was running into abstraction issues when it was defined in Uranium. Instead of trying to fight those, it's just easier to move it to Cura CURA-10717 --- cura/BackendPlugin.py | 74 +++++++++++++++++++ cura/CuraApplication.py | 9 +++ .../CuraEngineBackend/CuraEngineBackend.py | 16 +++- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 cura/BackendPlugin.py diff --git a/cura/BackendPlugin.py b/cura/BackendPlugin.py new file mode 100644 index 0000000000..6493f20487 --- /dev/null +++ b/cura/BackendPlugin.py @@ -0,0 +1,74 @@ +# Copyright (c) 2023 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import subprocess +from typing import Optional, List + +from UM.Logger import Logger +from UM.PluginObject import PluginObject + + +class BackendPlugin(PluginObject): + def __init__(self) -> None: + super().__init__() + self.__port: int = 0 + self._plugin_address: str = "127.0.0.1" + self._plugin_command: Optional[List[str]] = None + self._process = None + self._is_running = False + + def isRunning(self): + return self._is_running + + def setPort(self, port: int) -> None: + self.__port = port + + def getPort(self) -> int: + return self.__port + + def _validatePluginCommand(self) -> list[str]: + """ + Validate the plugin command and add the port parameter if it is missing. + + :return: A list of strings containing the validated plugin command. + """ + if not self._plugin_command or "--port" in self._plugin_command: + return self._plugin_command or [] + + return self._plugin_command + ["--port", str(self.__port)] + + def start(self) -> bool: + """ + Starts the backend_plugin process. + + :return: True if the plugin process started successfully, False otherwise. + """ + try: + # STDIN needs to be None because we provide no input, but communicate via a local socket instead. + # The NUL device sometimes doesn't exist on some computers. + self._process = subprocess.Popen(self._validatePluginCommand(), stdin = None) + self._is_running = True + return True + except PermissionError: + Logger.log("e", f"Couldn't start backend_plugin [{self._plugin_id}]: No permission to execute process.") + except FileNotFoundError: + Logger.logException("e", f"Unable to find backend_plugin executable [{self._plugin_id}]") + except BlockingIOError: + Logger.logException("e", f"Couldn't start backend_plugin [{self._plugin_id}]: Resource is temporarily unavailable") + except OSError as e: + Logger.logException("e", f"Couldn't start backend_plugin [{self._plugin_id}]: Operating system is blocking it (antivirus?)") + return False + + def stop(self) -> bool: + if not self._process: + self._is_running = False + return True # Nothing to stop + + try: + self._process.terminate() + return_code = self._process.wait() + self._is_running = False + Logger.log("d", f"Backend_plugin [{self._plugin_id}] was killed. Received return code {return_code}") + return True + except PermissionError: + Logger.log("e", "Unable to kill running engine. Access is denied.") + return False diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6b04503ebc..c96de7e50a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -205,6 +205,8 @@ class CuraApplication(QtApplication): self._cura_scene_controller = None self._machine_error_checker = None + self._backend_plugins: List[BackendPlugin] = [] + self._machine_settings_manager = MachineSettingsManager(self, parent = self) self._material_management_model = None self._quality_management_model = None @@ -792,6 +794,7 @@ class CuraApplication(QtApplication): self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_writer", self._addProfileWriter) + self._plugin_registry.addType("backend_plugin", self._addBackendPlugin) if Platform.isLinux(): lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions. @@ -1730,6 +1733,12 @@ class CuraApplication(QtApplication): def _addProfileWriter(self, profile_writer): pass + def _addBackendPlugin(self, backend_plugin: "BackendPlugin") -> None: + self._backend_plugins.append(backend_plugin) + + def getBackendPlugins(self) -> List["BackendPlugin"]: + return self._backend_plugins + @pyqtSlot("QSize") def setMinimumWindowSize(self, size): main_window = self.getMainWindow() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index f5d701f6f7..e53d29d9b9 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -70,7 +70,7 @@ class CuraEngineBackend(QObject, Backend): os.path.join(CuraApplication.getInstallPrefix(), "bin"), os.path.dirname(os.path.abspath(sys.executable)), ] - + self._last_backend_plugin_port = self._port + 1000 for path in search_path: engine_path = os.path.join(path, executable_name) if os.path.isfile(engine_path): @@ -176,6 +176,20 @@ class CuraEngineBackend(QObject, Backend): application.initializationFinished.connect(self.initialize) + def startPlugins(self) -> None: + """ + Ensure that all backend plugins are started + :return: + """ + backend_plugins = CuraApplication.getInstance().getBackendPlugins() + for backend_plugin in backend_plugins: + if backend_plugin.isRunning(): + continue + # Set the port to prevent plugins from using the same one. + backend_plugin.setPort(self._last_backend_plugin_port) + self.__last_backend_plugin_port += 1 + backend_plugin.start() + def _resetLastSliceTimeStats(self) -> None: self._time_start_process = None self._time_send_message = None From 5d74f2d2507fd1ee07d3a8a039f7cd098ad86956 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 11:30:49 +0200 Subject: [PATCH 07/12] Convert typing in CuraEngineBackend to modern style Boyscouting CURA-10717 --- .../CuraEngineBackend/CuraEngineBackend.py | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e53d29d9b9..903617d076 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -86,9 +86,9 @@ class CuraEngineBackend(QObject, Backend): self._default_engine_location = execpath break - application = CuraApplication.getInstance() #type: CuraApplication - self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] - self._machine_error_checker = None #type: Optional[MachineErrorChecker] + application: CuraApplication = CuraApplication.getInstance() + self._multi_build_plate_model: Optional[MultiBuildPlateModel] = None + self._machine_error_checker: Optional[MachineErrorChecker] = None if not self._default_engine_location: raise EnvironmentError("Could not find CuraEngine") @@ -99,13 +99,15 @@ class CuraEngineBackend(QObject, Backend): application.getPreferences().addPreference("backend/location", self._default_engine_location) # Workaround to disable layer view processing if layer view is not active. - self._layer_view_active = False #type: bool + self._layer_view_active: bool = False self._onActiveViewChanged() - self._stored_layer_data = [] # type: List[Arcus.PythonMessage] - self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob + self._stored_layer_data: List[Arcus.PythonMessage] = [] - self._scene = application.getController().getScene() #type: Scene + # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob + self._stored_optimized_layer_data: Dict[int, List[Arcus.PythonMessage]] = {} + + self._scene: Scene = application.getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -116,7 +118,7 @@ class CuraEngineBackend(QObject, Backend): # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished # to start the auto-slicing timer again. # - self._global_container_stack = None #type: Optional[ContainerStack] + self._global_container_stack: Optional[ContainerStack] = None # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage @@ -128,31 +130,32 @@ class CuraEngineBackend(QObject, Backend): self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage - self._start_slice_job = None #type: Optional[StartSliceJob] - self._start_slice_job_build_plate = None #type: Optional[int] - self._slicing = False #type: bool # Are we currently slicing? - self._restart = False #type: bool # Back-end is currently restarting? - self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything - self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. - self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers. - self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing? - self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not? + self._start_slice_job: Optional[StartSliceJob] = None + self._start_slice_job_build_plate: Optional[int] = None + self._slicing: bool = False # Are we currently slicing? + self._restart: bool = False # Back-end is currently restarting? + self._tool_active: bool = False # If a tool is active, some tasks do not have to do anything + self._always_restart: bool = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. + self._process_layers_job: Optional[ProcessSlicedLayersJob] = None # The currently active job to process layers, or None if it is not processing layers. + self._build_plates_to_be_sliced: List[int] = [] # what needs slicing? + self._engine_is_fresh: bool = True # Is the newly started engine used before or not? - self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer - self._error_message = None #type: Optional[Message] # Pop-up message that shows errors. - self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed - self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool) + self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer + self._error_message: Optional[Message] = None # Pop-up message that shows errors. + self._last_num_objects: Dict[int, int] = defaultdict(int) # Count number of objects to see if there is something changed + self._postponed_scene_change_sources: List[SceneNode] = [] # scene change is postponed (by a tool) - self._time_start_process = None #type: Optional[float] - self._is_disabled = False #type: bool + self._time_start_process: Optional[float] = None + self._is_disabled: bool = False application.getPreferences().addPreference("general/auto_slice", False) - self._use_timer = False #type: bool + self._use_timer: bool = False + # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. # This timer will group them up, and only slice for the last setting changed signal. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction. - self._change_timer = QTimer() #type: QTimer + self._change_timer: QTimer = QTimer() self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() @@ -172,7 +175,7 @@ class CuraEngineBackend(QObject, Backend): self._slicing_error_message.actionTriggered.connect(self._reportBackendError) self._resetLastSliceTimeStats() - self._snapshot = None #type: Optional[QImage] + self._snapshot: Optional[QImage] = None application.initializationFinished.connect(self.initialize) @@ -443,14 +446,14 @@ class CuraEngineBackend(QObject, Backend): Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") return extruders = ExtruderManager.getInstance().getActiveExtruderStacks() - error_keys = [] #type: List[str] + error_keys: List[str] = [] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: error_keys = self._global_container_stack.getErrorKeys() error_labels = set() for key in error_keys: - for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack. + for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack. definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key) if definitions: break #Found it! No need to continue search. @@ -580,7 +583,7 @@ class CuraEngineBackend(QObject, Backend): def _numObjectsPerBuildPlate(self) -> Dict[int, int]: """Return a dict with number of objects per build plate""" - num_objects = defaultdict(int) #type: Dict[int, int] + num_objects: Dict[int, int] = defaultdict(int) for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects if node.callDecoration("isSliceable"): From 19a4e0d2f834d22037080d688806aaa4ad69ae90 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 11:37:31 +0200 Subject: [PATCH 08/12] Fix bunch of comments violating max line length Boyscouting CURA-10717 --- .../CuraEngineBackend/CuraEngineBackend.py | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 903617d076..544780a03c 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -142,7 +142,9 @@ class CuraEngineBackend(QObject, Backend): self._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer self._error_message: Optional[Message] = None # Pop-up message that shows errors. - self._last_num_objects: Dict[int, int] = defaultdict(int) # Count number of objects to see if there is something changed + + # Count number of objects to see if there is something changed + self._last_num_objects: Dict[int, int] = defaultdict(int) self._postponed_scene_change_sources: List[SceneNode] = [] # scene change is postponed (by a tool) self._time_start_process: Optional[float] = None @@ -152,8 +154,8 @@ class CuraEngineBackend(QObject, Backend): self._use_timer: bool = False - # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. - # This timer will group them up, and only slice for the last setting changed signal. + # When you update a setting and other settings get changed through inheritance, many propertyChanged + # signals are fired. This timer will group them up, and only slice for the last setting changed signal. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction. self._change_timer: QTimer = QTimer() self._change_timer.setSingleShot(True) @@ -219,7 +221,8 @@ class CuraEngineBackend(QObject, Backend): application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() - # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash + # Extruder enable / disable. Actually wanted to use machine manager here, + # but the initialization order causes it to crash ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged) self.backendQuit.connect(self._onBackendQuit) @@ -256,7 +259,8 @@ class CuraEngineBackend(QObject, Backend): command += ["connect", "127.0.0.1:{0}".format(self._port), ""] parser = argparse.ArgumentParser(prog = "cura", add_help = False) - parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.") + parser.add_argument("--debug", action = "store_true", default = False, + help = "Turn on the debug mode by setting this option.") known_args = vars(parser.parse_known_args()[0]) if known_args["debug"]: command.append("-vvv") @@ -283,7 +287,8 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon. + if self._process_layers_job is not None: + # We were processing layers. Stop that, the layers are going to change soon. Logger.log("i", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None @@ -298,7 +303,7 @@ class CuraEngineBackend(QObject, Backend): self.markSliceAll() self.slice() - @call_on_qt_thread # must be called from the main thread because of OpenGL + @call_on_qt_thread # Must be called from the main thread because of OpenGL def _createSnapshot(self) -> None: self._snapshot = None if not CuraApplication.getInstance().isVisible: @@ -307,7 +312,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("i", "Creating thumbnail image (just before slice)...") try: self._snapshot = Snapshot.snapshot(width = 300, height = 300) - except: + except Exception: Logger.logException("w", "Failed to create snapshot image") self._snapshot = None # Failing to create thumbnail should not fail creation of UFP @@ -332,7 +337,8 @@ class CuraEngineBackend(QObject, Backend): return if not hasattr(self._scene, "gcode_dict"): - self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here. + self._scene.gcode_dict = {} # type: ignore + # We need to ignore type because we are creating the missing attribute here. # see if we really have to slice application = CuraApplication.getInstance() @@ -343,9 +349,9 @@ class CuraEngineBackend(QObject, Backend): self._stored_layer_data = [] - if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: - self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above. + self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore + # We need to ignore the type because we created this attribute above. Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: self.slice() @@ -354,7 +360,7 @@ class CuraEngineBackend(QObject, Backend): if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) - if self._process is None: # type: ignore + if self._process is None: # type: ignore self._createSocket() self.stopSlicing() self._engine_is_fresh = False # Yes we're going to use the engine @@ -362,7 +368,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] indexed by build plate number + self._scene.gcode_dict[build_plate_to_be_sliced] = [] # type: ignore #[] indexed by build plate number self._slicing = True self.slicingStarted.emit() @@ -394,14 +400,15 @@ class CuraEngineBackend(QObject, Backend): if CuraApplication.getInstance().getUseExternalBackend(): return - if self._process is not None: # type: ignore + if self._process is not None: # type: ignore Logger.log("d", "Killing engine process") try: - self._process.terminate() # type: ignore - Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore - self._process = None # type: ignore + self._process.terminate() # type: ignore + Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore + self._process = None # type: ignore - except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this. + except Exception as e: + # Terminating a process that is already terminating causes an exception, silently ignore this. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e)) def _onStartSliceCompleted(self, job: StartSliceJob) -> None: @@ -541,7 +548,7 @@ class CuraEngineBackend(QObject, Backend): # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) - # Notify the user that it's now up to the backend to do it's job + # Notify the user that it's now up to the backend to do its job self.setState(BackendState.Processing) # Handle time reporting. @@ -568,7 +575,8 @@ class CuraEngineBackend(QObject, Backend): self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: - self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically. + self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list # type: ignore + # We need to ignore type because we generate this attribute dynamically. if self._use_timer == enable_timer: return self._use_timer @@ -663,11 +671,13 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]: + if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, + Arcus.ErrorCode.ConnectionResetError, + Arcus.ErrorCode.Debug]: Logger.log("w", "A socket error caused the connection to be reset") # _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status - # needs to be updated. Otherwise backendState is "Unable To Slice" + # needs to be updated. Otherwise, backendState is "Unable To Slice" if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None: self._start_slice_job.setIsCancelled(False) @@ -689,7 +699,7 @@ class CuraEngineBackend(QObject, Backend): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: - # We can assume that all nodes have a parent as we're looping through the scene (and filter out root) + # We can assume that all nodes have a parent as we're looping through the scene and filter out root cast(SceneNode, node.getParent()).removeChild(node) def markSliceAll(self) -> None: @@ -718,7 +728,7 @@ class CuraEngineBackend(QObject, Backend): :param instance: The setting instance that has changed. :param property: The property of the setting instance that has changed. """ - if property == "value": # Only reslice if the value has changed. + if property == "value": # Only re-slice if the value has changed. self.needsSlicing() self._onChanged() @@ -787,8 +797,10 @@ class CuraEngineBackend(QObject, Backend): self._time_end_slice = time() try: - gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. - except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. + gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore + # We need to ignore the type because it was generated dynamically. + except KeyError: + # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. gcode_list = [] application = CuraApplication.getInstance() for index, line in enumerate(gcode_list): @@ -833,7 +845,8 @@ class CuraEngineBackend(QObject, Backend): try: self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. - except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. + except KeyError: + # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. pass # Throw the message away. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: @@ -845,7 +858,8 @@ class CuraEngineBackend(QObject, Backend): try: self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. - except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. + except KeyError: + # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. pass # Throw the message away. def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None: @@ -972,7 +986,8 @@ class CuraEngineBackend(QObject, Backend): view = CuraApplication.getInstance().getController().getActiveView() if view: active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate - if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. + if view.getPluginId() == "SimulationView": + # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. @@ -1024,7 +1039,8 @@ class CuraEngineBackend(QObject, Backend): self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine if self._global_container_stack: - self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. + # Note: Only starts slicing when the value changed. + self._global_container_stack.propertyChanged.connect(self._onSettingChanged) self._global_container_stack.containersChanged.connect(self._onChanged) for extruder in self._global_container_stack.extruderList: From bf32b83c82e061a2d9614051f0ac14d62214bace Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 11:38:21 +0200 Subject: [PATCH 09/12] Move signal definition to top of file It's easier to understand what is going on if the signal definitions are not spread out CURA-10717 --- .../CuraEngineBackend/CuraEngineBackend.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 544780a03c..39f3c75185 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -46,6 +46,19 @@ catalog = i18nCatalog("cura") class CuraEngineBackend(QObject, Backend): backendError = Signal() + printDurationMessage = Signal() + """Emitted when we get a message containing print duration and material amount. + + This also implies the slicing has finished. + :param time: The amount of time the print will take. + :param material_amount: The amount of material the print will use. + """ + slicingStarted = Signal() + """Emitted when the slicing process starts.""" + + slicingCancelled = Signal() + """Emitted when the slicing process is aborted forcefully.""" + def __init__(self) -> None: """Starts the back-end plug-in. @@ -267,19 +280,6 @@ class CuraEngineBackend(QObject, Backend): return command - printDurationMessage = Signal() - """Emitted when we get a message containing print duration and material amount. - - This also implies the slicing has finished. - :param time: The amount of time the print will take. - :param material_amount: The amount of material the print will use. - """ - slicingStarted = Signal() - """Emitted when the slicing process starts.""" - - slicingCancelled = Signal() - """Emitted when the slicing process is aborted forcefully.""" - @pyqtSlot() def stopSlicing(self) -> None: self.setState(BackendState.NotStarted) From 4aebd755b2aa6d814c87d26da1ecee19e8c6913a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 11:43:15 +0200 Subject: [PATCH 10/12] Clarify documentation of startBackendPlugin function CURA-10717 --- plugins/CuraEngineBackend/CuraEngineBackend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 39f3c75185..a7a0dfbdbf 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -197,6 +197,7 @@ class CuraEngineBackend(QObject, Backend): def startPlugins(self) -> None: """ Ensure that all backend plugins are started + It assigns unique ports to each plugin to avoid conflicts. :return: """ backend_plugins = CuraApplication.getInstance().getBackendPlugins() @@ -205,7 +206,7 @@ class CuraEngineBackend(QObject, Backend): continue # Set the port to prevent plugins from using the same one. backend_plugin.setPort(self._last_backend_plugin_port) - self.__last_backend_plugin_port += 1 + self._last_backend_plugin_port += 1 backend_plugin.start() def _resetLastSliceTimeStats(self) -> None: From 3037320b03b10bc67bcd3ce4522a21c6da5c25f3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 15:18:52 +0200 Subject: [PATCH 11/12] Send slotID's for backend_plugins CURA-10717 --- cura/BackendPlugin.py | 7 +++++++ plugins/CuraEngineBackend/StartSliceJob.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cura/BackendPlugin.py b/cura/BackendPlugin.py index 6493f20487..de7b3f29dc 100644 --- a/cura/BackendPlugin.py +++ b/cura/BackendPlugin.py @@ -15,6 +15,10 @@ class BackendPlugin(PluginObject): self._plugin_command: Optional[List[str]] = None self._process = None self._is_running = False + self._supported_slots: List[int] = [] + + def getSupportedSlots(self) -> List[int]: + return self._supported_slots def isRunning(self): return self._is_running @@ -25,6 +29,9 @@ class BackendPlugin(PluginObject): def getPort(self) -> int: return self.__port + def getAddress(self) -> str: + return self._plugin_address + def _validatePluginCommand(self) -> list[str]: """ Validate the plugin command and add the port parameter if it is missing. diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index d06136a2b4..f9fb7f6615 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -301,6 +301,19 @@ class StartSliceJob(Job): for extruder_stack in global_stack.extruderList: self._buildExtruderMessage(extruder_stack) + for plugin in CuraApplication.getInstance().getBackendPlugins(): + for slot in plugin.getSupportedSlots(): + # Right now we just send the message for every slot that we support. A single plugin can support + # multiple slots + # In the future the frontend will need to decide what slots that a plugin actually supports should + # also be used. For instance, if you have two plugins and each of them support a_generate and b_generate + # only one of each can actually be used (eg; plugin 1 does both, plugin 1 does a_generate and 2 does + # b_generate, etc). + plugin_message = self._slice_message.addRepeatedMessage("engine_plugins") + plugin_message.id = slot + plugin_message.address = plugin.getAddress() + plugin_message.port = plugin.getPort() + for group in filtered_object_groups: group_message = self._slice_message.addRepeatedMessage("object_lists") parent = group[0].getParent() From d37afe78801f14fa39578c51df65a60248ba868b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 11 Jul 2023 15:21:36 +0200 Subject: [PATCH 12/12] Actually call startPlugins before slicing CURA-10717 --- plugins/CuraEngineBackend/CuraEngineBackend.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index a7a0dfbdbf..ef073e6865 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -325,6 +325,8 @@ class CuraEngineBackend(QObject, Backend): self._createSnapshot() + self.startPlugins() + Logger.log("i", "Starting to slice...") self._time_start_process = time() if not self._build_plates_to_be_sliced: