Browse Source

Merge remote-tracking branch 'origin/CURA-10717_Engine_plugin_orchestration' into CURA-10475_engineplugin

# Conflicts:
#	plugins/CuraEngineBackend/Cura.proto
#	plugins/CuraEngineBackend/StartSliceJob.py
Jelle Spijker 1 year ago
parent
commit
4aae50396b

+ 30 - 33
.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,39 @@ 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 version>\cura.log` or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
-          MacOS: `$USER/Library/Application Support/cura/<Cura version>/cura.log`
-          Ubuntu/Linux: `$USER/.local/share/cura/<Cura version>/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 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)
+       * 🔵 **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 version>\cura.log`
+           MacOS: `$USER/Library/Application Support/cura/<Cura version>/cura.log`
+           Ubuntu/Linux: `$USER/.local/share/cura/<Cura version>/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
-

+ 7 - 0
.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:

+ 81 - 0
cura/BackendPlugin.py

@@ -0,0 +1,81 @@
+# 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
+        self._supported_slots: List[int] = []
+
+    def getSupportedSlots(self) -> List[int]:
+        return self._supported_slots
+
+    def isRunning(self):
+        return self._is_running
+
+    def setPort(self, port: int) -> None:
+        self.__port = port
+
+    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.
+
+        :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

+ 9 - 0
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()

+ 108 - 72
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.
 
@@ -70,7 +83,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):
@@ -86,9 +99,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 +112,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 +131,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 +143,34 @@ 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: int = 20000  # Maximum number of lines to buffer
+        self._error_message: Optional[Message] = None  # Pop-up message that shows errors.
 
-        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)
+        # 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 = 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
-        # 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.
+        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,10 +190,25 @@ 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)
 
+    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()
+        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
@@ -202,7 +235,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)
@@ -239,26 +273,14 @@ 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")
 
         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)
@@ -266,7 +288,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
@@ -281,7 +304,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:
@@ -290,7 +313,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
 
@@ -302,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:
@@ -315,7 +340,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()
@@ -326,9 +352,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()
@@ -337,7 +363,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
@@ -345,7 +371,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()
 
@@ -377,14 +403,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:
@@ -429,14 +456,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.
@@ -524,7 +551,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.
@@ -551,7 +578,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
@@ -566,7 +594,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"):
@@ -646,11 +674,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)
 
@@ -672,7 +702,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:
@@ -701,7 +731,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()
 
@@ -770,8 +800,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):
@@ -816,7 +848,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:
@@ -828,7 +861,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:
@@ -955,7 +989,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.
@@ -1007,7 +1042,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:

+ 12 - 15
plugins/CuraEngineBackend/StartSliceJob.py

@@ -302,21 +302,18 @@ class StartSliceJob(Job):
         for extruder_stack in global_stack.extruderList:
             self._buildExtruderMessage(extruder_stack)
 
-        # EnginePlugins
-        # TODO: don't hardcode them
-        # Ports: are chosen based on https://stackoverflow.com/questions/10476987/best-tcp-port-number-range-for-internal-applications
-
-        plugins = {
-            0: {"address": os.environ.get("SIMPLIFY_ADDRESS", "localhost"), "port": os.environ.get("SIMPLIFY_PORT", 33700)} if os.environ.get("SIMPLIFY_ENABLE") is not None else None,
-            1: {"address": os.environ.get("POSTPROCESS_ADDRESS", "localhost"), "port": os.environ.get("POSTPROCESS_PORT", 33701)} if os.environ.get("POSTPROCESS_ENABLE") is not None else None,
-        }
-
-        for plugin, connection in plugins.items():
-            plugin_message = self._slice_message.addRepeatedMessage("engine_plugins")
-            plugin_message.id = plugin
-            if connection:
-                plugin_message.address = connection["address"]
-                plugin_message.port = connection["port"]
+        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")