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

@ -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 = 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._time_start_process = None #type: Optional[float]
self._is_disabled = False #type: bool
# 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
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:

View file

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