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

# Conflicts:
#	plugins/CuraEngineBackend/Cura.proto
#	plugins/CuraEngineBackend/StartSliceJob.py
This commit is contained in:
Jelle Spijker 2023-07-12 18:35:21 +02:00
commit 4aae50396b
No known key found for this signature in database
GPG key ID: 034D1C0527888B65
6 changed files with 247 additions and 120 deletions

View file

@ -5,17 +5,17 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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. 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 - type: input
attributes: attributes:
label: Application Version label: Cura Version
description: The version of Cura this issue occurs with. description: The version of Cura this issue occurs with.
placeholder: 5.3.0 placeholder: 5.4.0
validations: validations:
required: true required: true
- type: input - type: input
@ -28,14 +28,14 @@ body:
- type: input - type: input
attributes: attributes:
label: Printer label: Printer
description: Which printer was selected in Cura? description: Which printer was selected in Cura? It also helps to mention if you made any firmware modifications to your printer.
placeholder: Ultimaker S7 placeholder: Ultimaker S7 / Creality CR-10 with Klipper
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Reproduction steps label: Reproduction steps
description: Tell us what you did! description: Share what you did, so we can reproduce it
placeholder: | placeholder: |
1. Something you did 1. Something you did
2. Something you did next 2. Something you did next
@ -44,42 +44,39 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Actual results label: Actual results
description: What happens after the above steps have been followed. description: What happens after the above steps have been followed?
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Expected results 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: validations:
required: true required: true
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please be sure to add the following files: ### Please add the following files when they are related to...
* To save a project file go to File -> Save project. * 🔵 **The quality of your print**
Please make sure to .zip your project file. Please add **a Project File**. It contains the printer and settings we need for troubleshooting.
For big files, you may need to use [WeTransfer](https://wetransfer.com/) or similar file-sharing sites. To save a project file go to File -> Save project.
G-code files are not project files! 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.
Before you share, please think to yourself. Is this a model that can be shared? 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. ![Alt Text](https://user-images.githubusercontent.com/40423138/240616958-5a9751f2-bd34-4808-9752-6fde2e27516e.gif)
* A **log file** for crashes and similar issues. * 🔵 **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: 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` Windows: `%APPDATA%\cura\<Cura version>\cura.log`
MacOS: `$USER/Library/Application Support/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` 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 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
- type: textarea - type: textarea
attributes: attributes:
label: Additional information & file uploads label: Add your .zip and screenshots here ⬇️
description: You can add these files and additional information that is relevant to the issue in the comments below. description: You can add the zip file and additional information that is relevant to the issue in the comments below.
validations: validations:
required: true required: true

View file

@ -17,6 +17,13 @@ on:
- 'conandata.yml' - 'conandata.yml'
- 'GitVersion.yml' - 'GitVersion.yml'
- '*.jinja' - '*.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: jobs:
update-translations: update-translations:

81
cura/BackendPlugin.py Normal file
View file

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

View file

@ -205,6 +205,8 @@ class CuraApplication(QtApplication):
self._cura_scene_controller = None self._cura_scene_controller = None
self._machine_error_checker = None self._machine_error_checker = None
self._backend_plugins: List[BackendPlugin] = []
self._machine_settings_manager = MachineSettingsManager(self, parent = self) self._machine_settings_manager = MachineSettingsManager(self, parent = self)
self._material_management_model = None self._material_management_model = None
self._quality_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_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter) self._plugin_registry.addType("profile_writer", self._addProfileWriter)
self._plugin_registry.addType("backend_plugin", self._addBackendPlugin)
if Platform.isLinux(): if Platform.isLinux():
lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions. lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
@ -1730,6 +1733,12 @@ class CuraApplication(QtApplication):
def _addProfileWriter(self, profile_writer): def _addProfileWriter(self, profile_writer):
pass 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") @pyqtSlot("QSize")
def setMinimumWindowSize(self, size): def setMinimumWindowSize(self, size):
main_window = self.getMainWindow() main_window = self.getMainWindow()

View file

@ -46,6 +46,19 @@ catalog = i18nCatalog("cura")
class CuraEngineBackend(QObject, Backend): class CuraEngineBackend(QObject, Backend):
backendError = Signal() 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: def __init__(self) -> None:
"""Starts the back-end plug-in. """Starts the back-end plug-in.
@ -70,7 +83,7 @@ class CuraEngineBackend(QObject, Backend):
os.path.join(CuraApplication.getInstallPrefix(), "bin"), os.path.join(CuraApplication.getInstallPrefix(), "bin"),
os.path.dirname(os.path.abspath(sys.executable)), os.path.dirname(os.path.abspath(sys.executable)),
] ]
self._last_backend_plugin_port = self._port + 1000
for path in search_path: for path in search_path:
engine_path = os.path.join(path, executable_name) engine_path = os.path.join(path, executable_name)
if os.path.isfile(engine_path): if os.path.isfile(engine_path):
@ -86,9 +99,9 @@ class CuraEngineBackend(QObject, Backend):
self._default_engine_location = execpath self._default_engine_location = execpath
break break
application = CuraApplication.getInstance() #type: CuraApplication application: CuraApplication = CuraApplication.getInstance()
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] self._multi_build_plate_model: Optional[MultiBuildPlateModel] = None
self._machine_error_checker = None #type: Optional[MachineErrorChecker] self._machine_error_checker: Optional[MachineErrorChecker] = None
if not self._default_engine_location: if not self._default_engine_location:
raise EnvironmentError("Could not find CuraEngine") raise EnvironmentError("Could not find CuraEngine")
@ -99,13 +112,15 @@ class CuraEngineBackend(QObject, Backend):
application.getPreferences().addPreference("backend/location", self._default_engine_location) application.getPreferences().addPreference("backend/location", self._default_engine_location)
# Workaround to disable layer view processing if layer view is not active. # 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._onActiveViewChanged()
self._stored_layer_data = [] # type: List[Arcus.PythonMessage] self._stored_layer_data: 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._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) self._scene.sceneChanged.connect(self._onSceneChanged)
# Triggers for auto-slicing. Auto-slicing is triggered as follows: # 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 # 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. # 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. # Listeners for receiving messages from the back-end.
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage 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.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None #type: Optional[StartSliceJob] self._start_slice_job: Optional[StartSliceJob] = None
self._start_slice_job_build_plate = None #type: Optional[int] self._start_slice_job_build_plate: Optional[int] = None
self._slicing = False #type: bool # Are we currently slicing? self._slicing: bool = False # Are we currently slicing?
self._restart = False #type: bool # Back-end is currently restarting? self._restart: bool = False # 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._tool_active: bool = False # 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._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 = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers. 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 = [] #type: List[int] # what needs slicing? self._build_plates_to_be_sliced: List[int] = [] # what needs slicing?
self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not? 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._backend_log_max_lines: int = 20000 # Maximum number of lines to buffer
self._error_message = None #type: Optional[Message] # Pop-up message that shows errors. self._error_message: Optional[Message] = None # 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._time_start_process = None #type: Optional[float] # Count number of objects to see if there is something changed
self._is_disabled = False #type: bool 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
self._is_disabled: bool = False
application.getPreferences().addPreference("general/auto_slice", 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. # 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. # 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.setSingleShot(True)
self._change_timer.setInterval(500) self._change_timer.setInterval(500)
self.determineAutoSlicing() self.determineAutoSlicing()
@ -172,10 +190,25 @@ class CuraEngineBackend(QObject, Backend):
self._slicing_error_message.actionTriggered.connect(self._reportBackendError) self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
self._resetLastSliceTimeStats() self._resetLastSliceTimeStats()
self._snapshot = None #type: Optional[QImage] self._snapshot: Optional[QImage] = None
application.initializationFinished.connect(self.initialize) 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: def _resetLastSliceTimeStats(self) -> None:
self._time_start_process = None self._time_start_process = None
self._time_send_message = None self._time_send_message = None
@ -202,7 +235,8 @@ class CuraEngineBackend(QObject, Backend):
application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
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) ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
self.backendQuit.connect(self._onBackendQuit) self.backendQuit.connect(self._onBackendQuit)
@ -239,26 +273,14 @@ class CuraEngineBackend(QObject, Backend):
command += ["connect", "127.0.0.1:{0}".format(self._port), ""] command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False) 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]) known_args = vars(parser.parse_known_args()[0])
if known_args["debug"]: if known_args["debug"]:
command.append("-vvv") command.append("-vvv")
return command 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() @pyqtSlot()
def stopSlicing(self) -> None: def stopSlicing(self) -> None:
self.setState(BackendState.NotStarted) self.setState(BackendState.NotStarted)
@ -266,7 +288,8 @@ class CuraEngineBackend(QObject, Backend):
self._terminate() self._terminate()
self._createSocket() 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...") Logger.log("i", "Aborting process layers job...")
self._process_layers_job.abort() self._process_layers_job.abort()
self._process_layers_job = None self._process_layers_job = None
@ -281,7 +304,7 @@ class CuraEngineBackend(QObject, Backend):
self.markSliceAll() self.markSliceAll()
self.slice() 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: def _createSnapshot(self) -> None:
self._snapshot = None self._snapshot = None
if not CuraApplication.getInstance().isVisible: if not CuraApplication.getInstance().isVisible:
@ -290,7 +313,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("i", "Creating thumbnail image (just before slice)...") Logger.log("i", "Creating thumbnail image (just before slice)...")
try: try:
self._snapshot = Snapshot.snapshot(width = 300, height = 300) self._snapshot = Snapshot.snapshot(width = 300, height = 300)
except: except Exception:
Logger.logException("w", "Failed to create snapshot image") Logger.logException("w", "Failed to create snapshot image")
self._snapshot = None # Failing to create thumbnail should not fail creation of UFP self._snapshot = None # Failing to create thumbnail should not fail creation of UFP
@ -302,6 +325,8 @@ class CuraEngineBackend(QObject, Backend):
self._createSnapshot() self._createSnapshot()
self.startPlugins()
Logger.log("i", "Starting to slice...") Logger.log("i", "Starting to slice...")
self._time_start_process = time() self._time_start_process = time()
if not self._build_plates_to_be_sliced: if not self._build_plates_to_be_sliced:
@ -315,7 +340,8 @@ class CuraEngineBackend(QObject, Backend):
return return
if not hasattr(self._scene, "gcode_dict"): 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 # see if we really have to slice
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
@ -326,9 +352,9 @@ class CuraEngineBackend(QObject, Backend):
self._stored_layer_data = [] self._stored_layer_data = []
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: 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) 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: if self._build_plates_to_be_sliced:
self.slice() self.slice()
@ -345,7 +371,7 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0) self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted) 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._slicing = True
self.slicingStarted.emit() self.slicingStarted.emit()
@ -384,7 +410,8 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # 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 = 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)) Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
def _onStartSliceCompleted(self, job: StartSliceJob) -> None: def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
@ -429,7 +456,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
return return
extruders = ExtruderManager.getInstance().getActiveExtruderStacks() extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
error_keys = [] #type: List[str] error_keys: List[str] = []
for extruder in extruders: for extruder in extruders:
error_keys.extend(extruder.getErrorKeys()) error_keys.extend(extruder.getErrorKeys())
if not extruders: if not extruders:
@ -524,7 +551,7 @@ class CuraEngineBackend(QObject, Backend):
# Preparation completed, send it to the backend. # Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage()) 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) self.setState(BackendState.Processing)
# Handle time reporting. # Handle time reporting.
@ -551,7 +578,8 @@ class CuraEngineBackend(QObject, Backend):
self._is_disabled = True self._is_disabled = True
gcode_list = node.callDecoration("getGCodeList") gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None: 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: if self._use_timer == enable_timer:
return self._use_timer return self._use_timer
@ -566,7 +594,7 @@ class CuraEngineBackend(QObject, Backend):
def _numObjectsPerBuildPlate(self) -> Dict[int, int]: def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
"""Return a dict with number of objects per build plate""" """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()): for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects # Only count sliceable objects
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
@ -646,11 +674,13 @@ class CuraEngineBackend(QObject, Backend):
self._terminate() self._terminate()
self._createSocket() 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") 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 # _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: if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
self._start_slice_job.setIsCancelled(False) self._start_slice_job.setIsCancelled(False)
@ -672,7 +702,7 @@ class CuraEngineBackend(QObject, Backend):
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"): if node.callDecoration("getLayerData"):
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: 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) cast(SceneNode, node.getParent()).removeChild(node)
def markSliceAll(self) -> None: def markSliceAll(self) -> None:
@ -701,7 +731,7 @@ class CuraEngineBackend(QObject, Backend):
:param instance: The setting instance that has changed. :param instance: The setting instance that has changed.
:param property: The property of 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.needsSlicing()
self._onChanged() self._onChanged()
@ -770,8 +800,10 @@ class CuraEngineBackend(QObject, Backend):
self._time_end_slice = time() self._time_end_slice = time()
try: try:
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. # 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 = [] gcode_list = []
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
for index, line in enumerate(gcode_list): for index, line in enumerate(gcode_list):
@ -816,7 +848,8 @@ class CuraEngineBackend(QObject, Backend):
try: 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. 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. pass # Throw the message away.
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
@ -828,7 +861,8 @@ class CuraEngineBackend(QObject, Backend):
try: 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. 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. pass # Throw the message away.
def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None: def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
@ -955,7 +989,8 @@ class CuraEngineBackend(QObject, Backend):
view = CuraApplication.getInstance().getController().getActiveView() view = CuraApplication.getInstance().getController().getActiveView()
if view: if view:
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate 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 self._layer_view_active = True
# There is data and we're not slicing at the moment # 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. # 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 self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine
if self._global_container_stack: 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) self._global_container_stack.containersChanged.connect(self._onChanged)
for extruder in self._global_container_stack.extruderList: for extruder in self._global_container_stack.extruderList:

View file

@ -302,21 +302,18 @@ class StartSliceJob(Job):
for extruder_stack in global_stack.extruderList: for extruder_stack in global_stack.extruderList:
self._buildExtruderMessage(extruder_stack) self._buildExtruderMessage(extruder_stack)
# EnginePlugins for plugin in CuraApplication.getInstance().getBackendPlugins():
# TODO: don't hardcode them for slot in plugin.getSupportedSlots():
# Ports: are chosen based on https://stackoverflow.com/questions/10476987/best-tcp-port-number-range-for-internal-applications # Right now we just send the message for every slot that we support. A single plugin can support
# multiple slots
plugins = { # In the future the frontend will need to decide what slots that a plugin actually supports should
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, # also be used. For instance, if you have two plugins and each of them support a_generate and b_generate
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, # only one of each can actually be used (eg; plugin 1 does both, plugin 1 does a_generate and 2 does
} # b_generate, etc).
for plugin, connection in plugins.items():
plugin_message = self._slice_message.addRepeatedMessage("engine_plugins") plugin_message = self._slice_message.addRepeatedMessage("engine_plugins")
plugin_message.id = plugin plugin_message.id = slot
if connection: plugin_message.address = plugin.getAddress()
plugin_message.address = connection["address"] plugin_message.port = plugin.getPort()
plugin_message.port = connection["port"]
for group in filtered_object_groups: for group in filtered_object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists") group_message = self._slice_message.addRepeatedMessage("object_lists")