mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-15 02:37:49 -06:00
Merge pull request #7551 from Ultimaker/doxygen_to_restructuredtext_comments
Converted doxygen style comments to reStructuredText style
This commit is contained in:
commit
98587a9008
224 changed files with 5521 additions and 3874 deletions
|
@ -32,8 +32,9 @@ except ImportError:
|
|||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
||||
class ThreeMFReader(MeshReader):
|
||||
"""Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -55,13 +56,17 @@ class ThreeMFReader(MeshReader):
|
|||
return Matrix()
|
||||
|
||||
split_transformation = transformation.split()
|
||||
## Transformation is saved as:
|
||||
## M00 M01 M02 0.0
|
||||
## M10 M11 M12 0.0
|
||||
## M20 M21 M22 0.0
|
||||
## M30 M31 M32 1.0
|
||||
## We switch the row & cols as that is how everyone else uses matrices!
|
||||
temp_mat = Matrix()
|
||||
"""Transformation is saved as:
|
||||
M00 M01 M02 0.0
|
||||
|
||||
M10 M11 M12 0.0
|
||||
|
||||
M20 M21 M22 0.0
|
||||
|
||||
M30 M31 M32 1.0
|
||||
We switch the row & cols as that is how everyone else uses matrices!
|
||||
"""
|
||||
# Rotation & Scale
|
||||
temp_mat._data[0, 0] = split_transformation[0]
|
||||
temp_mat._data[1, 0] = split_transformation[1]
|
||||
|
@ -80,9 +85,11 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
return temp_mat
|
||||
|
||||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
# \returns Scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
|
||||
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
|
||||
:returns: Scene node.
|
||||
"""
|
||||
try:
|
||||
node_name = savitar_node.getName()
|
||||
node_id = savitar_node.getId()
|
||||
|
@ -243,15 +250,17 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
return result
|
||||
|
||||
## Create a scale vector based on a unit string.
|
||||
# The core spec defines the following:
|
||||
# * micron
|
||||
# * millimeter (default)
|
||||
# * centimeter
|
||||
# * inch
|
||||
# * foot
|
||||
# * meter
|
||||
def _getScaleFromUnit(self, unit: Optional[str]) -> Vector:
|
||||
"""Create a scale vector based on a unit string.
|
||||
|
||||
.. The core spec defines the following:
|
||||
* micron
|
||||
* millimeter (default)
|
||||
* centimeter
|
||||
* inch
|
||||
* foot
|
||||
* meter
|
||||
"""
|
||||
conversion_to_mm = {
|
||||
"micron": 0.001,
|
||||
"millimeter": 1,
|
||||
|
|
|
@ -89,8 +89,9 @@ class ExtruderInfo:
|
|||
self.intent_info = None
|
||||
|
||||
|
||||
## Base implementation for reading 3MF workspace files.
|
||||
class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
"""Base implementation for reading 3MF workspace files."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -130,18 +131,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._old_new_materials = {}
|
||||
self._machine_info = None
|
||||
|
||||
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
||||
def getNewId(self, old_id: str):
|
||||
"""Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
|
||||
|
||||
This has nothing to do with speed, but with getting consistent new naming for instances & objects.
|
||||
"""
|
||||
if old_id not in self._id_mapping:
|
||||
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
||||
return self._id_mapping[old_id]
|
||||
|
||||
## Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files.
|
||||
#
|
||||
# In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg".
|
||||
#
|
||||
def _determineGlobalAndExtruderStackFiles(self, project_file_name: str, file_list: List[str]) -> Tuple[str, List[str]]:
|
||||
"""Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files.
|
||||
|
||||
In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg".
|
||||
"""
|
||||
|
||||
archive = zipfile.ZipFile(project_file_name, "r")
|
||||
|
||||
global_stack_file_list = [name for name in file_list if name.endswith(self._global_stack_suffix)]
|
||||
|
@ -181,10 +185,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
return global_stack_file_list[0], extruder_stack_file_list
|
||||
|
||||
## read some info so we can make decisions
|
||||
# \param file_name
|
||||
# \param show_dialog In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
|
||||
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
||||
"""Read some info so we can make decisions
|
||||
|
||||
:param file_name:
|
||||
:param show_dialog: In case we use preRead() to check if a file is a valid project file,
|
||||
we don't want to show a dialog.
|
||||
"""
|
||||
self._clearState()
|
||||
|
||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||
|
@ -578,15 +585,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
## Read the project file
|
||||
# Add all the definitions / materials / quality changes that do not exist yet. Then it loads
|
||||
# all the stacks into the container registry. In some cases it will reuse the container for the global stack.
|
||||
# It handles old style project files containing .stack.cfg as well as new style project files
|
||||
# containing global.cfg / extruder.cfg
|
||||
#
|
||||
# \param file_name
|
||||
@call_on_qt_thread
|
||||
def read(self, file_name):
|
||||
"""Read the project file
|
||||
|
||||
Add all the definitions / materials / quality changes that do not exist yet. Then it loads
|
||||
all the stacks into the container registry. In some cases it will reuse the container for the global stack.
|
||||
It handles old style project files containing .stack.cfg as well as new style project files
|
||||
containing global.cfg / extruder.cfg
|
||||
|
||||
:param file_name:
|
||||
"""
|
||||
application = CuraApplication.getInstance()
|
||||
|
||||
try:
|
||||
|
@ -868,19 +877,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
self._machine_info.quality_changes_info.name = quality_changes_name
|
||||
|
||||
## Helper class to create a new quality changes profile.
|
||||
#
|
||||
# This will then later be filled with the appropriate data.
|
||||
# \param quality_type The quality type of the new profile.
|
||||
# \param intent_category The intent category of the new profile.
|
||||
# \param name The name for the profile. This will later be made unique so
|
||||
# it doesn't need to be unique yet.
|
||||
# \param global_stack The global stack showing the configuration that the
|
||||
# profile should be created for.
|
||||
# \param extruder_stack The extruder stack showing the configuration that
|
||||
# the profile should be created for. If this is None, it will be created
|
||||
# for the global stack.
|
||||
def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer:
|
||||
"""Helper class to create a new quality changes profile.
|
||||
|
||||
This will then later be filled with the appropriate data.
|
||||
|
||||
:param quality_type: The quality type of the new profile.
|
||||
:param intent_category: The intent category of the new profile.
|
||||
:param name: The name for the profile. This will later be made unique so
|
||||
it doesn't need to be unique yet.
|
||||
:param global_stack: The global stack showing the configuration that the
|
||||
profile should be created for.
|
||||
:param extruder_stack: The extruder stack showing the configuration that
|
||||
the profile should be created for. If this is None, it will be created
|
||||
for the global stack.
|
||||
"""
|
||||
|
||||
container_registry = CuraApplication.getInstance().getContainerRegistry()
|
||||
base_id = global_stack.definition.getId() if extruder_stack is None else extruder_stack.getId()
|
||||
new_id = base_id + "_" + name
|
||||
|
@ -1089,9 +1101,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
def _getXmlProfileClass(self):
|
||||
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
|
||||
|
||||
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
|
||||
@staticmethod
|
||||
def _getContainerIdListFromSerialized(serialized):
|
||||
"""Get the list of ID's of all containers in a container stack by partially parsing it's serialized data."""
|
||||
|
||||
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
|
||||
parser.read_string(serialized)
|
||||
|
||||
|
|
|
@ -229,9 +229,10 @@ class WorkspaceDialog(QObject):
|
|||
if key in self._result:
|
||||
self._result[key] = strategy
|
||||
|
||||
## Close the backend: otherwise one could end up with "Slicing..."
|
||||
@pyqtSlot()
|
||||
def closeBackend(self):
|
||||
"""Close the backend: otherwise one could end up with "Slicing..."""
|
||||
|
||||
Application.getInstance().getBackend().close()
|
||||
|
||||
def setMaterialConflict(self, material_conflict):
|
||||
|
@ -283,8 +284,9 @@ class WorkspaceDialog(QObject):
|
|||
self.showDialogSignal.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
## Used to notify the dialog so the lock can be released.
|
||||
def notifyClosed(self):
|
||||
"""Used to notify the dialog so the lock can be released."""
|
||||
|
||||
self._result = {} # The result should be cleared before hide, because after it is released the main thread lock
|
||||
self._visible = False
|
||||
try:
|
||||
|
@ -319,8 +321,9 @@ class WorkspaceDialog(QObject):
|
|||
self._view.hide()
|
||||
self.hide()
|
||||
|
||||
## Block thread until the dialog is closed.
|
||||
def waitForClose(self):
|
||||
"""Block thread until the dialog is closed."""
|
||||
|
||||
if self._visible:
|
||||
if threading.current_thread() != threading.main_thread():
|
||||
self._lock.acquire()
|
||||
|
|
|
@ -33,7 +33,7 @@ def getMetaData() -> Dict:
|
|||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
return metaData
|
||||
|
||||
|
||||
|
|
|
@ -107,11 +107,13 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
import json
|
||||
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
|
||||
|
||||
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||
# \param container That follows the \type{ContainerInterface} to archive.
|
||||
# \param archive The archive to write to.
|
||||
@staticmethod
|
||||
def _writeContainerToArchive(container, archive):
|
||||
"""Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||
|
||||
:param container: That follows the :type{ContainerInterface} to archive.
|
||||
:param archive: The archive to write to.
|
||||
"""
|
||||
if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
||||
return # Empty file, do nothing.
|
||||
|
||||
|
|
|
@ -60,15 +60,19 @@ class ThreeMFWriter(MeshWriter):
|
|||
result += str(matrix._data[2, 3])
|
||||
return result
|
||||
|
||||
## Should we store the archive
|
||||
# Note that if this is true, the archive will not be closed.
|
||||
# The object that set this parameter is then responsible for closing it correctly!
|
||||
def setStoreArchive(self, store_archive):
|
||||
"""Should we store the archive
|
||||
|
||||
Note that if this is true, the archive will not be closed.
|
||||
The object that set this parameter is then responsible for closing it correctly!
|
||||
"""
|
||||
self._store_archive = store_archive
|
||||
|
||||
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
# \returns Uranium Scene node.
|
||||
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
|
||||
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||
|
||||
:returns: Uranium Scene node.
|
||||
"""
|
||||
if not isinstance(um_node, SceneNode):
|
||||
return None
|
||||
|
||||
|
|
|
@ -147,13 +147,13 @@ class AMFReader(MeshReader):
|
|||
|
||||
return group_node
|
||||
|
||||
## Converts a Trimesh to Uranium's MeshData.
|
||||
# \param tri_node A Trimesh containing the contents of a file that was
|
||||
# just read.
|
||||
# \param file_name The full original filename used to watch for changes
|
||||
# \return Mesh data from the Trimesh in a way that Uranium can understand
|
||||
# it.
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData:
|
||||
"""Converts a Trimesh to Uranium's MeshData.
|
||||
|
||||
:param tri_node: A Trimesh containing the contents of a file that was just read.
|
||||
:param file_name: The full original filename used to watch for changes
|
||||
:return: Mesh data from the Trimesh in a way that Uranium can understand it.
|
||||
"""
|
||||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
|
|
|
@ -42,12 +42,14 @@ catalog = i18nCatalog("cura")
|
|||
class CuraEngineBackend(QObject, Backend):
|
||||
backendError = Signal()
|
||||
|
||||
## Starts the back-end plug-in.
|
||||
#
|
||||
# This registers all the signal listeners and prepares for communication
|
||||
# with the back-end in general.
|
||||
# CuraEngineBackend is exposed to qml as well.
|
||||
def __init__(self) -> None:
|
||||
"""Starts the back-end plug-in.
|
||||
|
||||
This registers all the signal listeners and prepares for communication
|
||||
with the back-end in general.
|
||||
CuraEngineBackend is exposed to qml as well.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
# Find out where the engine is located, and how it is called.
|
||||
# This depends on how Cura is packaged and which OS we are running on.
|
||||
|
@ -177,18 +179,22 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._machine_error_checker = self._application.getMachineErrorChecker()
|
||||
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
|
||||
|
||||
## Terminate the engine process.
|
||||
#
|
||||
# This function should terminate the engine process.
|
||||
# Called when closing the application.
|
||||
def close(self) -> None:
|
||||
"""Terminate the engine process.
|
||||
|
||||
This function should terminate the engine process.
|
||||
Called when closing the application.
|
||||
"""
|
||||
|
||||
# Terminate CuraEngine if it is still running at this point
|
||||
self._terminate()
|
||||
|
||||
## Get the command that is used to call the engine.
|
||||
# This is useful for debugging and used to actually start the engine.
|
||||
# \return list of commands and args / parameters.
|
||||
def getEngineCommand(self) -> List[str]:
|
||||
"""Get the command that is used to call the engine.
|
||||
|
||||
This is useful for debugging and used to actually start the engine.
|
||||
:return: list of commands and args / parameters.
|
||||
"""
|
||||
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||
|
||||
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
||||
|
@ -199,17 +205,18 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
return command
|
||||
|
||||
## 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.
|
||||
printDurationMessage = Signal()
|
||||
"""Emitted when we get a message containing print duration and material amount.
|
||||
|
||||
## Emitted when the slicing process starts.
|
||||
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."""
|
||||
|
||||
## Emitted when the slicing process is aborted forcefully.
|
||||
slicingCancelled = Signal()
|
||||
"""Emitted when the slicing process is aborted forcefully."""
|
||||
|
||||
@pyqtSlot()
|
||||
def stopSlicing(self) -> None:
|
||||
|
@ -226,14 +233,16 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
|
||||
## Manually triggers a reslice
|
||||
@pyqtSlot()
|
||||
def forceSlice(self) -> None:
|
||||
"""Manually triggers a reslice"""
|
||||
|
||||
self.markSliceAll()
|
||||
self.slice()
|
||||
|
||||
## Perform a slice of the scene.
|
||||
def slice(self) -> None:
|
||||
"""Perform a slice of the scene."""
|
||||
|
||||
Logger.log("i", "Starting to slice...")
|
||||
self._slice_start_time = time()
|
||||
if not self._build_plates_to_be_sliced:
|
||||
|
@ -289,9 +298,11 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._start_slice_job.start()
|
||||
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
|
||||
|
||||
## Terminate the engine process.
|
||||
# Start the engine process by calling _createSocket()
|
||||
def _terminate(self) -> None:
|
||||
"""Terminate the engine process.
|
||||
|
||||
Start the engine process by calling _createSocket()
|
||||
"""
|
||||
self._slicing = False
|
||||
self._stored_layer_data = []
|
||||
if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
|
||||
|
@ -316,15 +327,17 @@ class CuraEngineBackend(QObject, Backend):
|
|||
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))
|
||||
|
||||
## Event handler to call when the job to initiate the slicing process is
|
||||
# completed.
|
||||
#
|
||||
# When the start slice job is successfully completed, it will be happily
|
||||
# slicing. This function handles any errors that may occur during the
|
||||
# bootstrapping of a slice job.
|
||||
#
|
||||
# \param job The start slice job that was just finished.
|
||||
def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
|
||||
"""Event handler to call when the job to initiate the slicing process is
|
||||
|
||||
completed.
|
||||
|
||||
When the start slice job is successfully completed, it will be happily
|
||||
slicing. This function handles any errors that may occur during the
|
||||
bootstrapping of a slice job.
|
||||
|
||||
:param job: The start slice job that was just finished.
|
||||
"""
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
|
||||
|
@ -443,11 +456,13 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if self._slice_start_time:
|
||||
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
||||
|
||||
## Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
|
||||
# It disables when
|
||||
# - preference auto slice is off
|
||||
# - decorator isBlockSlicing is found (used in g-code reader)
|
||||
def determineAutoSlicing(self) -> bool:
|
||||
"""Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
|
||||
|
||||
It disables when:
|
||||
- preference auto slice is off
|
||||
- decorator isBlockSlicing is found (used in g-code reader)
|
||||
"""
|
||||
enable_timer = True
|
||||
self._is_disabled = False
|
||||
|
||||
|
@ -472,8 +487,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.disableTimer()
|
||||
return False
|
||||
|
||||
## Return a dict with number of objects per build plate
|
||||
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
|
||||
"""Return a dict with number of objects per build plate"""
|
||||
|
||||
num_objects = defaultdict(int) #type: Dict[int, int]
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
# Only count sliceable objects
|
||||
|
@ -483,12 +499,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
num_objects[build_plate_number] += 1
|
||||
return num_objects
|
||||
|
||||
## Listener for when the scene has changed.
|
||||
#
|
||||
# This should start a slice if the scene is now ready to slice.
|
||||
#
|
||||
# \param source The scene node that was changed.
|
||||
def _onSceneChanged(self, source: SceneNode) -> None:
|
||||
"""Listener for when the scene has changed.
|
||||
|
||||
This should start a slice if the scene is now ready to slice.
|
||||
|
||||
:param source: The scene node that was changed.
|
||||
"""
|
||||
|
||||
if not source.callDecoration("isSliceable") and source != self._scene.getRoot():
|
||||
return
|
||||
|
||||
|
@ -536,10 +554,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
self._invokeSlice()
|
||||
|
||||
## Called when an error occurs in the socket connection towards the engine.
|
||||
#
|
||||
# \param error The exception that occurred.
|
||||
def _onSocketError(self, error: Arcus.Error) -> None:
|
||||
"""Called when an error occurs in the socket connection towards the engine.
|
||||
|
||||
:param error: The exception that occurred.
|
||||
"""
|
||||
|
||||
if self._application.isShuttingDown():
|
||||
return
|
||||
|
||||
|
@ -567,8 +587,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
break
|
||||
return has_slicable
|
||||
|
||||
## Remove old layer data (if any)
|
||||
def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
|
||||
"""Remove old layer data (if any)"""
|
||||
|
||||
# Clear out any old gcode
|
||||
self._scene.gcode_dict = {} # type: ignore
|
||||
|
||||
|
@ -583,8 +604,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||
|
||||
## Convenient function: mark everything to slice, emit state and clear layer data
|
||||
def needsSlicing(self) -> None:
|
||||
"""Convenient function: mark everything to slice, emit state and clear layer data"""
|
||||
|
||||
# CURA-6604: If there's no slicable object, do not (try to) trigger slice, which will clear all the current
|
||||
# gcode. This can break Gcode file loading if it tries to remove it afterwards.
|
||||
if not self.hasSlicableObject():
|
||||
|
@ -597,10 +619,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
# With manually having to slice, we want to clear the old invalid layer data.
|
||||
self._clearLayerData()
|
||||
|
||||
## A setting has changed, so check if we must reslice.
|
||||
# \param instance The setting instance that has changed.
|
||||
# \param property The property of the setting instance that has changed.
|
||||
def _onSettingChanged(self, instance: SettingInstance, property: str) -> None:
|
||||
"""A setting has changed, so check if we must reslice.
|
||||
|
||||
: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.
|
||||
self.needsSlicing()
|
||||
self._onChanged()
|
||||
|
@ -618,25 +642,31 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.needsSlicing()
|
||||
self._onChanged()
|
||||
|
||||
## Called when a sliced layer data message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing sliced layer data.
|
||||
def _onLayerMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when a sliced layer data message is received from the engine.
|
||||
|
||||
:param message: The protobuf message containing sliced layer data.
|
||||
"""
|
||||
|
||||
self._stored_layer_data.append(message)
|
||||
|
||||
## Called when an optimized sliced layer data message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing sliced layer data.
|
||||
def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when an optimized sliced layer data message is received from the engine.
|
||||
|
||||
:param message: The protobuf message containing sliced layer data.
|
||||
"""
|
||||
|
||||
if self._start_slice_job_build_plate is not None:
|
||||
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
|
||||
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
|
||||
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
|
||||
|
||||
## Called when a progress message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing the slicing progress.
|
||||
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when a progress message is received from the engine.
|
||||
|
||||
:param message: The protobuf message containing the slicing progress.
|
||||
"""
|
||||
|
||||
self.processingProgress.emit(message.amount)
|
||||
self.setState(BackendState.Processing)
|
||||
|
||||
|
@ -653,10 +683,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
else:
|
||||
self._change_timer.start()
|
||||
|
||||
## Called when the engine sends a message that slicing is finished.
|
||||
#
|
||||
# \param message The protobuf message signalling that slicing is finished.
|
||||
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when the engine sends a message that slicing is finished.
|
||||
|
||||
:param message: The protobuf message signalling that slicing is finished.
|
||||
"""
|
||||
|
||||
self.setState(BackendState.Done)
|
||||
self.processingProgress.emit(1.0)
|
||||
|
||||
|
@ -698,27 +730,32 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode
|
||||
self._invokeSlice()
|
||||
|
||||
## Called when a g-code message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing g-code, encoded as UTF-8.
|
||||
def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when a g-code message is received from the engine.
|
||||
|
||||
:param message: The protobuf message containing g-code, encoded as UTF-8.
|
||||
"""
|
||||
|
||||
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.
|
||||
pass # Throw the message away.
|
||||
|
||||
## Called when a g-code prefix message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing the g-code prefix,
|
||||
# encoded as UTF-8.
|
||||
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when a g-code prefix message is received from the engine.
|
||||
|
||||
:param message: The protobuf message containing the g-code prefix,
|
||||
encoded as UTF-8.
|
||||
"""
|
||||
|
||||
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.
|
||||
pass # Throw the message away.
|
||||
|
||||
## Creates a new socket connection.
|
||||
def _createSocket(self, protocol_file: str = None) -> None:
|
||||
"""Creates a new socket connection."""
|
||||
|
||||
if not protocol_file:
|
||||
if not self.getPluginId():
|
||||
Logger.error("Can't create socket before CuraEngineBackend plug-in is registered.")
|
||||
|
@ -731,10 +768,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
super()._createSocket(protocol_file)
|
||||
self._engine_is_fresh = True
|
||||
|
||||
## Called when anything has changed to the stuff that needs to be sliced.
|
||||
#
|
||||
# This indicates that we should probably re-slice soon.
|
||||
def _onChanged(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Called when anything has changed to the stuff that needs to be sliced.
|
||||
|
||||
This indicates that we should probably re-slice soon.
|
||||
"""
|
||||
|
||||
self.needsSlicing()
|
||||
if self._use_timer:
|
||||
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||
|
@ -748,11 +787,13 @@ class CuraEngineBackend(QObject, Backend):
|
|||
else:
|
||||
self._change_timer.start()
|
||||
|
||||
## Called when a print time message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing the print time per feature and
|
||||
# material amount per extruder
|
||||
def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None:
|
||||
"""Called when a print time message is received from the engine.
|
||||
|
||||
:param message: The protobuf message containing the print time per feature and
|
||||
material amount per extruder
|
||||
"""
|
||||
|
||||
material_amounts = []
|
||||
for index in range(message.repeatedMessageCount("materialEstimates")):
|
||||
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
||||
|
@ -760,10 +801,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
times = self._parseMessagePrintTimes(message)
|
||||
self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts)
|
||||
|
||||
## Called for parsing message to retrieve estimated time per feature
|
||||
#
|
||||
# \param message The protobuf message containing the print time per feature
|
||||
def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]:
|
||||
"""Called for parsing message to retrieve estimated time per feature
|
||||
|
||||
:param message: The protobuf message containing the print time per feature
|
||||
"""
|
||||
|
||||
result = {
|
||||
"inset_0": message.time_inset_0,
|
||||
"inset_x": message.time_inset_x,
|
||||
|
@ -780,19 +823,22 @@ class CuraEngineBackend(QObject, Backend):
|
|||
}
|
||||
return result
|
||||
|
||||
## Called when the back-end connects to the front-end.
|
||||
def _onBackendConnected(self) -> None:
|
||||
"""Called when the back-end connects to the front-end."""
|
||||
|
||||
if self._restart:
|
||||
self._restart = False
|
||||
self._onChanged()
|
||||
|
||||
## Called when the user starts using some tool.
|
||||
#
|
||||
# When the user starts using a tool, we should pause slicing to prevent
|
||||
# continuously slicing while the user is dragging some tool handle.
|
||||
#
|
||||
# \param tool The tool that the user is using.
|
||||
def _onToolOperationStarted(self, tool: Tool) -> None:
|
||||
"""Called when the user starts using some tool.
|
||||
|
||||
When the user starts using a tool, we should pause slicing to prevent
|
||||
continuously slicing while the user is dragging some tool handle.
|
||||
|
||||
:param tool: The tool that the user is using.
|
||||
"""
|
||||
|
||||
self._tool_active = True # Do not react on scene change
|
||||
self.disableTimer()
|
||||
# Restart engine as soon as possible, we know we want to slice afterwards
|
||||
|
@ -800,12 +846,14 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._terminate()
|
||||
self._createSocket()
|
||||
|
||||
## Called when the user stops using some tool.
|
||||
#
|
||||
# This indicates that we can safely start slicing again.
|
||||
#
|
||||
# \param tool The tool that the user was using.
|
||||
def _onToolOperationStopped(self, tool: Tool) -> None:
|
||||
"""Called when the user stops using some tool.
|
||||
|
||||
This indicates that we can safely start slicing again.
|
||||
|
||||
:param tool: The tool that the user was using.
|
||||
"""
|
||||
|
||||
self._tool_active = False # React on scene change again
|
||||
self.determineAutoSlicing() # Switch timer on if appropriate
|
||||
# Process all the postponed scene changes
|
||||
|
@ -819,8 +867,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
|
||||
## Called when the user changes the active view mode.
|
||||
def _onActiveViewChanged(self) -> None:
|
||||
"""Called when the user changes the active view mode."""
|
||||
|
||||
view = self._application.getController().getActiveView()
|
||||
if view:
|
||||
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
||||
|
@ -838,17 +887,20 @@ class CuraEngineBackend(QObject, Backend):
|
|||
else:
|
||||
self._layer_view_active = False
|
||||
|
||||
## Called when the back-end self-terminates.
|
||||
#
|
||||
# We should reset our state and start listening for new connections.
|
||||
def _onBackendQuit(self) -> None:
|
||||
"""Called when the back-end self-terminates.
|
||||
|
||||
We should reset our state and start listening for new connections.
|
||||
"""
|
||||
|
||||
if not self._restart:
|
||||
if self._process: # type: ignore
|
||||
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore
|
||||
self._process = None # type: ignore
|
||||
|
||||
## Called when the global container stack changes
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
"""Called when the global container stack changes"""
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
|
||||
self._global_container_stack.containersChanged.disconnect(self._onChanged)
|
||||
|
@ -877,15 +929,18 @@ class CuraEngineBackend(QObject, Backend):
|
|||
Logger.log("d", "See if there is more to slice(2)...")
|
||||
self._invokeSlice()
|
||||
|
||||
## Connect slice function to timer.
|
||||
def enableTimer(self) -> None:
|
||||
"""Connect slice function to timer."""
|
||||
|
||||
if not self._use_timer:
|
||||
self._change_timer.timeout.connect(self.slice)
|
||||
self._use_timer = True
|
||||
|
||||
## Disconnect slice function from timer.
|
||||
# This means that slicing will not be triggered automatically
|
||||
def disableTimer(self) -> None:
|
||||
"""Disconnect slice function from timer.
|
||||
|
||||
This means that slicing will not be triggered automatically
|
||||
"""
|
||||
if self._use_timer:
|
||||
self._use_timer = False
|
||||
self._change_timer.timeout.disconnect(self.slice)
|
||||
|
@ -897,8 +952,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if auto_slice:
|
||||
self._change_timer.start()
|
||||
|
||||
## Tickle the backend so in case of auto slicing, it starts the timer.
|
||||
def tickle(self) -> None:
|
||||
"""Tickle the backend so in case of auto slicing, it starts the timer."""
|
||||
|
||||
if self._use_timer:
|
||||
self._change_timer.start()
|
||||
|
||||
|
|
|
@ -28,10 +28,12 @@ from cura.Machines.Models.ExtrudersModel import ExtrudersModel
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Return a 4-tuple with floats 0-1 representing the html color code
|
||||
#
|
||||
# \param color_code html color code, i.e. "#FF0000" -> red
|
||||
def colorCodeToRGBA(color_code):
|
||||
"""Return a 4-tuple with floats 0-1 representing the html color code
|
||||
|
||||
:param color_code: html color code, i.e. "#FF0000" -> red
|
||||
"""
|
||||
|
||||
if color_code is None:
|
||||
Logger.log("w", "Unable to convert color code, returning default")
|
||||
return [0, 0, 0, 1]
|
||||
|
@ -51,13 +53,15 @@ class ProcessSlicedLayersJob(Job):
|
|||
self._abort_requested = False
|
||||
self._build_plate_number = None
|
||||
|
||||
## Aborts the processing of layers.
|
||||
#
|
||||
# This abort is made on a best-effort basis, meaning that the actual
|
||||
# job thread will check once in a while to see whether an abort is
|
||||
# requested and then stop processing by itself. There is no guarantee
|
||||
# that the abort will stop the job any time soon or even at all.
|
||||
def abort(self):
|
||||
"""Aborts the processing of layers.
|
||||
|
||||
This abort is made on a best-effort basis, meaning that the actual
|
||||
job thread will check once in a while to see whether an abort is
|
||||
requested and then stop processing by itself. There is no guarantee
|
||||
that the abort will stop the job any time soon or even at all.
|
||||
"""
|
||||
|
||||
self._abort_requested = True
|
||||
|
||||
def setBuildPlate(self, new_value):
|
||||
|
|
|
@ -42,8 +42,9 @@ class StartJobResult(IntEnum):
|
|||
ObjectsWithDisabledExtruder = 8
|
||||
|
||||
|
||||
## Formatter class that handles token expansion in start/end gcode
|
||||
class GcodeStartEndFormatter(Formatter):
|
||||
"""Formatter class that handles token expansion in start/end gcode"""
|
||||
|
||||
def __init__(self, default_extruder_nr: int = -1) -> None:
|
||||
super().__init__()
|
||||
self._default_extruder_nr = default_extruder_nr
|
||||
|
@ -84,8 +85,9 @@ class GcodeStartEndFormatter(Formatter):
|
|||
return value
|
||||
|
||||
|
||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||
class StartSliceJob(Job):
|
||||
"""Job class that builds up the message of scene data to send to CuraEngine."""
|
||||
|
||||
def __init__(self, slice_message: Arcus.PythonMessage) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -102,9 +104,10 @@ class StartSliceJob(Job):
|
|||
def setBuildPlate(self, build_plate_number: int) -> None:
|
||||
self._build_plate_number = build_plate_number
|
||||
|
||||
## Check if a stack has any errors.
|
||||
## returns true if it has errors, false otherwise.
|
||||
def _checkStackForErrors(self, stack: ContainerStack) -> bool:
|
||||
"""Check if a stack has any errors."""
|
||||
|
||||
"""returns true if it has errors, false otherwise."""
|
||||
|
||||
top_of_stack = cast(InstanceContainer, stack.getTop()) # Cache for efficiency.
|
||||
changed_setting_keys = top_of_stack.getAllKeys()
|
||||
|
@ -134,8 +137,9 @@ class StartSliceJob(Job):
|
|||
|
||||
return False
|
||||
|
||||
## Runs the job that initiates the slicing.
|
||||
def run(self) -> None:
|
||||
"""Runs the job that initiates the slicing."""
|
||||
|
||||
if self._build_plate_number is None:
|
||||
self.setResult(StartJobResult.Error)
|
||||
return
|
||||
|
@ -338,14 +342,14 @@ class StartSliceJob(Job):
|
|||
def setIsCancelled(self, value: bool):
|
||||
self._is_cancelled = value
|
||||
|
||||
## Creates a dictionary of tokens to replace in g-code pieces.
|
||||
#
|
||||
# This indicates what should be replaced in the start and end g-codes.
|
||||
# \param stack The stack to get the settings from to replace the tokens
|
||||
# with.
|
||||
# \return A dictionary of replacement tokens to the values they should be
|
||||
# replaced with.
|
||||
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]:
|
||||
"""Creates a dictionary of tokens to replace in g-code pieces.
|
||||
|
||||
This indicates what should be replaced in the start and end g-codes.
|
||||
:param stack: The stack to get the settings from to replace the tokens with.
|
||||
:return: A dictionary of replacement tokens to the values they should be replaced with.
|
||||
"""
|
||||
|
||||
result = {}
|
||||
for key in stack.getAllKeys():
|
||||
value = stack.getProperty(key, "value")
|
||||
|
@ -373,10 +377,12 @@ class StartSliceJob(Job):
|
|||
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
|
||||
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
|
||||
|
||||
## Replace setting tokens in a piece of g-code.
|
||||
# \param value A piece of g-code to replace tokens in.
|
||||
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
||||
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str:
|
||||
"""Replace setting tokens in a piece of g-code.
|
||||
|
||||
:param value: A piece of g-code to replace tokens in.
|
||||
:param default_extruder_nr: Stack nr to use when no stack nr is specified, defaults to the global stack
|
||||
"""
|
||||
if not self._all_extruders_settings:
|
||||
self._cacheAllExtruderSettings()
|
||||
|
||||
|
@ -392,8 +398,9 @@ class StartSliceJob(Job):
|
|||
Logger.logException("w", "Unable to do token replacement on start/end g-code")
|
||||
return str(value)
|
||||
|
||||
## Create extruder message from stack
|
||||
def _buildExtruderMessage(self, stack: ContainerStack) -> None:
|
||||
"""Create extruder message from stack"""
|
||||
|
||||
message = self._slice_message.addRepeatedMessage("extruders")
|
||||
message.id = int(stack.getMetaDataEntry("position"))
|
||||
if not self._all_extruders_settings:
|
||||
|
@ -422,11 +429,13 @@ class StartSliceJob(Job):
|
|||
setting.value = str(value).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Sends all global settings to the engine.
|
||||
#
|
||||
# The settings are taken from the global stack. This does not include any
|
||||
# per-extruder settings or per-object settings.
|
||||
def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None:
|
||||
"""Sends all global settings to the engine.
|
||||
|
||||
The settings are taken from the global stack. This does not include any
|
||||
per-extruder settings or per-object settings.
|
||||
"""
|
||||
|
||||
if not self._all_extruders_settings:
|
||||
self._cacheAllExtruderSettings()
|
||||
|
||||
|
@ -460,15 +469,16 @@ class StartSliceJob(Job):
|
|||
setting_message.value = str(value).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Sends for some settings which extruder they should fallback to if not
|
||||
# set.
|
||||
#
|
||||
# This is only set for settings that have the limit_to_extruder
|
||||
# property.
|
||||
#
|
||||
# \param stack The global stack with all settings, from which to read the
|
||||
# limit_to_extruder property.
|
||||
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None:
|
||||
"""Sends for some settings which extruder they should fallback to if not set.
|
||||
|
||||
This is only set for settings that have the limit_to_extruder
|
||||
property.
|
||||
|
||||
:param stack: The global stack with all settings, from which to read the
|
||||
limit_to_extruder property.
|
||||
"""
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
||||
if extruder_position >= 0: # Set to a specific extruder.
|
||||
|
@ -477,10 +487,13 @@ class StartSliceJob(Job):
|
|||
setting_extruder.extruder = extruder_position
|
||||
Job.yieldThread()
|
||||
|
||||
## Check if a node has per object settings and ensure that they are set correctly in the message
|
||||
# \param node Node to check.
|
||||
# \param message object_lists message to put the per object settings in
|
||||
def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage):
|
||||
"""Check if a node has per object settings and ensure that they are set correctly in the message
|
||||
|
||||
:param node: Node to check.
|
||||
:param message: object_lists message to put the per object settings in
|
||||
"""
|
||||
|
||||
stack = node.callDecoration("getStack")
|
||||
|
||||
# Check if the node has a stack attached to it and the stack has any settings in the top container.
|
||||
|
@ -516,10 +529,13 @@ class StartSliceJob(Job):
|
|||
|
||||
Job.yieldThread()
|
||||
|
||||
## Recursive function to put all settings that require each other for value changes in a list
|
||||
# \param relations_set Set of keys of settings that are influenced
|
||||
# \param relations list of relation objects that need to be checked.
|
||||
def _addRelations(self, relations_set: Set[str], relations: List[SettingRelation]):
|
||||
"""Recursive function to put all settings that require each other for value changes in a list
|
||||
|
||||
:param relations_set: Set of keys of settings that are influenced
|
||||
:param relations: list of relation objects that need to be checked.
|
||||
"""
|
||||
|
||||
for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations):
|
||||
if relation.type == RelationType.RequiresTarget:
|
||||
continue
|
||||
|
|
|
@ -13,23 +13,30 @@ from cura.ReaderWriters.ProfileReader import ProfileReader
|
|||
|
||||
import zipfile
|
||||
|
||||
## A plugin that reads profile data from Cura profile files.
|
||||
#
|
||||
# It reads a profile from a .curaprofile file, and returns it as a profile
|
||||
# instance.
|
||||
|
||||
class CuraProfileReader(ProfileReader):
|
||||
## Initialises the cura profile reader.
|
||||
# This does nothing since the only other function is basically stateless.
|
||||
"""A plugin that reads profile data from Cura profile files.
|
||||
|
||||
It reads a profile from a .curaprofile file, and returns it as a profile
|
||||
instance.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialises the cura profile reader.
|
||||
|
||||
This does nothing since the only other function is basically stateless.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
## Reads a cura profile from a file and returns it.
|
||||
#
|
||||
# \param file_name The file to read the cura profile from.
|
||||
# \return The cura profiles that were in the file, if any. If the file
|
||||
# could not be read or didn't contain a valid profile, ``None`` is
|
||||
# returned.
|
||||
def read(self, file_name: str) -> List[Optional[InstanceContainer]]:
|
||||
"""Reads a cura profile from a file and returns it.
|
||||
|
||||
:param file_name: The file to read the cura profile from.
|
||||
:return: The cura profiles that were in the file, if any. If the file
|
||||
could not be read or didn't contain a valid profile, ``None`` is
|
||||
returned.
|
||||
"""
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(file_name, "r") as archive:
|
||||
results = [] # type: List[Optional[InstanceContainer]]
|
||||
|
@ -50,13 +57,14 @@ class CuraProfileReader(ProfileReader):
|
|||
serialized_bytes = fhandle.read()
|
||||
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized_bytes, file_name)]
|
||||
|
||||
## Convert a profile from an old Cura to this Cura if needed.
|
||||
#
|
||||
# \param serialized The profile data to convert in the serialized on-disk
|
||||
# format.
|
||||
# \param profile_id The name of the profile.
|
||||
# \return List of serialized profile strings and matching profile names.
|
||||
def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]:
|
||||
"""Convert a profile from an old Cura to this Cura if needed.
|
||||
|
||||
:param serialized: The profile data to convert in the serialized on-disk format.
|
||||
:param profile_id: The name of the profile.
|
||||
:return: List of serialized profile strings and matching profile names.
|
||||
"""
|
||||
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
|
@ -75,12 +83,14 @@ class CuraProfileReader(ProfileReader):
|
|||
else:
|
||||
return [(serialized, profile_id)]
|
||||
|
||||
## Load a profile from a serialized string.
|
||||
#
|
||||
# \param serialized The profile data to read.
|
||||
# \param profile_id The name of the profile.
|
||||
# \return The profile that was stored in the string.
|
||||
def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]:
|
||||
"""Load a profile from a serialized string.
|
||||
|
||||
:param serialized: The profile data to read.
|
||||
:param profile_id: The name of the profile.
|
||||
:return: The profile that was stored in the string.
|
||||
"""
|
||||
|
||||
# Create an empty profile.
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.setMetaDataEntry("type", "quality_changes")
|
||||
|
@ -102,13 +112,15 @@ class CuraProfileReader(ProfileReader):
|
|||
profile.setMetaDataEntry("definition", active_quality_definition)
|
||||
return profile
|
||||
|
||||
## Upgrade a serialized profile to the current profile format.
|
||||
#
|
||||
# \param serialized The profile data to convert.
|
||||
# \param profile_id The name of the profile.
|
||||
# \param source_version The profile version of 'serialized'.
|
||||
# \return List of serialized profile strings and matching profile names.
|
||||
def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]:
|
||||
"""Upgrade a serialized profile to the current profile format.
|
||||
|
||||
:param serialized: The profile data to convert.
|
||||
:param profile_id: The name of the profile.
|
||||
:param source_version: The profile version of 'serialized'.
|
||||
:return: List of serialized profile strings and matching profile names.
|
||||
"""
|
||||
|
||||
source_version = main_version * 1000000 + setting_version
|
||||
|
||||
from UM.VersionUpgradeManager import VersionUpgradeManager
|
||||
|
|
|
@ -6,15 +6,18 @@ from UM.Logger import Logger
|
|||
from cura.ReaderWriters.ProfileWriter import ProfileWriter
|
||||
import zipfile
|
||||
|
||||
## Writes profiles to Cura's own profile format with config files.
|
||||
class CuraProfileWriter(ProfileWriter):
|
||||
## Writes a profile to the specified file path.
|
||||
#
|
||||
# \param path \type{string} The file to output to.
|
||||
# \param profiles \type{Profile} \type{List} The profile(s) to write to that file.
|
||||
# \return \code True \endcode if the writing was successful, or \code
|
||||
# False \endcode if it wasn't.
|
||||
"""Writes profiles to Cura's own profile format with config files."""
|
||||
|
||||
def write(self, path, profiles):
|
||||
"""Writes a profile to the specified file path.
|
||||
|
||||
:param path: :type{string} The file to output to.
|
||||
:param profiles: :type{Profile} :type{List} The profile(s) to write to that file.
|
||||
:return: True if the writing was successful, or
|
||||
False if it wasn't.
|
||||
"""
|
||||
|
||||
if type(profiles) != list:
|
||||
profiles = [profiles]
|
||||
|
||||
|
|
|
@ -18,10 +18,12 @@ from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
|||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## This Extension checks for new versions of the firmware based on the latest checked version number.
|
||||
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
|
||||
# to change it to work for other applications.
|
||||
class FirmwareUpdateChecker(Extension):
|
||||
"""This Extension checks for new versions of the firmware based on the latest checked version number.
|
||||
|
||||
The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
|
||||
to change it to work for other applications.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
@ -35,8 +37,9 @@ class FirmwareUpdateChecker(Extension):
|
|||
self._check_job = None
|
||||
self._checked_printer_names = set() # type: Set[str]
|
||||
|
||||
## Callback for the message that is spawned when there is a new version.
|
||||
def _onActionTriggered(self, message, action):
|
||||
"""Callback for the message that is spawned when there is a new version."""
|
||||
|
||||
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
|
||||
machine_id = message.getMachineId()
|
||||
download_url = message.getDownloadUrl()
|
||||
|
@ -57,13 +60,15 @@ class FirmwareUpdateChecker(Extension):
|
|||
def _onJobFinished(self, *args, **kwargs):
|
||||
self._check_job = None
|
||||
|
||||
## Connect with software.ultimaker.com, load latest.version and check version info.
|
||||
# If the version info is different from the current version, spawn a message to
|
||||
# allow the user to download it.
|
||||
#
|
||||
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
|
||||
# This is used when checking for a new firmware version at startup.
|
||||
def checkFirmwareVersion(self, container = None, silent = False):
|
||||
"""Connect with software.ultimaker.com, load latest.version and check version info.
|
||||
|
||||
If the version info is different from the current version, spawn a message to
|
||||
allow the user to download it.
|
||||
|
||||
:param silent: type(boolean) Suppresses messages other than "new version found" messages.
|
||||
This is used when checking for a new firmware version at startup.
|
||||
"""
|
||||
container_name = container.definition.getName()
|
||||
if container_name in self._checked_printer_names:
|
||||
return
|
||||
|
|
|
@ -21,8 +21,9 @@ from UM.i18n import i18nCatalog
|
|||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## This job checks if there is an update available on the provided URL.
|
||||
class FirmwareUpdateCheckerJob(Job):
|
||||
"""This job checks if there is an update available on the provided URL."""
|
||||
|
||||
STRING_ZERO_VERSION = "0.0.0"
|
||||
STRING_EPSILON_VERSION = "0.0.1"
|
||||
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
||||
|
|
|
@ -19,8 +19,10 @@ if MYPY:
|
|||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Upgrade the firmware of a machine by USB with this action.
|
||||
|
||||
class FirmwareUpdaterMachineAction(MachineAction):
|
||||
"""Upgrade the firmware of a machine by USB with this action."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
|
||||
self._qml_url = "FirmwareUpdaterMachineAction.qml"
|
||||
|
|
|
@ -7,10 +7,13 @@ from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementin
|
|||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database.
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
## A file reader that reads gzipped g-code.
|
||||
#
|
||||
# If you're zipping g-code, you might as well use gzip!
|
||||
|
||||
class GCodeGzReader(MeshReader):
|
||||
"""A file reader that reads gzipped g-code.
|
||||
|
||||
If you're zipping g-code, you might as well use gzip!
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
MimeTypeDatabase.addMimeType(
|
||||
|
|
|
@ -13,26 +13,31 @@ from UM.Scene.SceneNode import SceneNode #For typing.
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## A file writer that writes gzipped g-code.
|
||||
#
|
||||
# If you're zipping g-code, you might as well use gzip!
|
||||
|
||||
class GCodeGzWriter(MeshWriter):
|
||||
"""A file writer that writes gzipped g-code.
|
||||
|
||||
If you're zipping g-code, you might as well use gzip!
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(add_to_recent_files = False)
|
||||
|
||||
## Writes the gzipped g-code to a stream.
|
||||
#
|
||||
# Note that even though the function accepts a collection of nodes, the
|
||||
# entire scene is always written to the file since it is not possible to
|
||||
# separate the g-code for just specific nodes.
|
||||
#
|
||||
# \param stream The stream to write the gzipped g-code to.
|
||||
# \param nodes This is ignored.
|
||||
# \param mode Additional information on what type of stream to use. This
|
||||
# must always be binary mode.
|
||||
# \return Whether the write was successful.
|
||||
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||
"""Writes the gzipped g-code to a stream.
|
||||
|
||||
Note that even though the function accepts a collection of nodes, the
|
||||
entire scene is always written to the file since it is not possible to
|
||||
separate the g-code for just specific nodes.
|
||||
|
||||
:param stream: The stream to write the gzipped g-code to.
|
||||
:param nodes: This is ignored.
|
||||
:param mode: Additional information on what type of stream to use. This
|
||||
must always be binary mode.
|
||||
:return: Whether the write was successful.
|
||||
"""
|
||||
|
||||
if mode != MeshWriter.OutputMode.BinaryMode:
|
||||
Logger.log("e", "GCodeGzWriter does not support text mode.")
|
||||
self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode."))
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import re #Regular expressions for parsing escape characters in the settings.
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
@ -12,40 +13,48 @@ catalog = i18nCatalog("cura")
|
|||
|
||||
from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException
|
||||
|
||||
## A class that reads profile data from g-code files.
|
||||
#
|
||||
# It reads the profile data from g-code files and stores it in a new profile.
|
||||
# This class currently does not process the rest of the g-code in any way.
|
||||
class GCodeProfileReader(ProfileReader):
|
||||
## The file format version of the serialized g-code.
|
||||
#
|
||||
# It can only read settings with the same version as the version it was
|
||||
# written with. If the file format is changed in a way that breaks reverse
|
||||
# compatibility, increment this version number!
|
||||
version = 3
|
||||
"""A class that reads profile data from g-code files.
|
||||
|
||||
It reads the profile data from g-code files and stores it in a new profile.
|
||||
This class currently does not process the rest of the g-code in any way.
|
||||
"""
|
||||
|
||||
version = 3
|
||||
"""The file format version of the serialized g-code.
|
||||
|
||||
It can only read settings with the same version as the version it was
|
||||
written with. If the file format is changed in a way that breaks reverse
|
||||
compatibility, increment this version number!
|
||||
"""
|
||||
|
||||
## Dictionary that defines how characters are escaped when embedded in
|
||||
# g-code.
|
||||
#
|
||||
# Note that the keys of this dictionary are regex strings. The values are
|
||||
# not.
|
||||
escape_characters = {
|
||||
re.escape("\\\\"): "\\", #The escape character.
|
||||
re.escape("\\n"): "\n", #Newlines. They break off the comment.
|
||||
re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors.
|
||||
}
|
||||
"""Dictionary that defines how characters are escaped when embedded in
|
||||
|
||||
g-code.
|
||||
|
||||
Note that the keys of this dictionary are regex strings. The values are
|
||||
not.
|
||||
"""
|
||||
|
||||
## Initialises the g-code reader as a profile reader.
|
||||
def __init__(self):
|
||||
"""Initialises the g-code reader as a profile reader."""
|
||||
|
||||
super().__init__()
|
||||
|
||||
## Reads a g-code file, loading the profile from it.
|
||||
#
|
||||
# \param file_name The name of the file to read the profile from.
|
||||
# \return The profile that was in the specified file, if any. If the
|
||||
# specified file was no g-code or contained no parsable profile, \code
|
||||
# None \endcode is returned.
|
||||
def read(self, file_name):
|
||||
"""Reads a g-code file, loading the profile from it.
|
||||
|
||||
:param file_name: The name of the file to read the profile from.
|
||||
:return: The profile that was in the specified file, if any. If the
|
||||
specified file was no g-code or contained no parsable profile,
|
||||
None is returned.
|
||||
"""
|
||||
|
||||
if file_name.split(".")[-1] != "gcode":
|
||||
return None
|
||||
|
||||
|
@ -94,22 +103,28 @@ class GCodeProfileReader(ProfileReader):
|
|||
profiles.append(readQualityProfileFromString(profile_string))
|
||||
return profiles
|
||||
|
||||
## Unescape a string which has been escaped for use in a gcode comment.
|
||||
#
|
||||
# \param string The string to unescape.
|
||||
# \return \type{str} The unscaped string.
|
||||
def unescapeGcodeComment(string):
|
||||
|
||||
def unescapeGcodeComment(string: str) -> str:
|
||||
"""Unescape a string which has been escaped for use in a gcode comment.
|
||||
|
||||
:param string: The string to unescape.
|
||||
:return: The unescaped string.
|
||||
"""
|
||||
|
||||
# Un-escape the serialized profile.
|
||||
pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
|
||||
|
||||
# Perform the replacement with a regular expression.
|
||||
return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
|
||||
|
||||
## Read in a profile from a serialized string.
|
||||
#
|
||||
# \param profile_string The profile data in serialized form.
|
||||
# \return \type{Profile} the resulting Profile object or None if it could not be read.
|
||||
def readQualityProfileFromString(profile_string):
|
||||
|
||||
def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]:
|
||||
"""Read in a profile from a serialized string.
|
||||
|
||||
:param profile_string: The profile data in serialized form.
|
||||
:return: The resulting Profile object or None if it could not be read.
|
||||
"""
|
||||
|
||||
# Create an empty profile - the id and name will be changed by the ContainerRegistry
|
||||
profile = InstanceContainer("")
|
||||
try:
|
||||
|
|
|
@ -28,9 +28,8 @@ PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optiona
|
|||
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
|
||||
|
||||
|
||||
## This parser is intended to interpret the common firmware codes among all the
|
||||
# different flavors
|
||||
class FlavorParser:
|
||||
"""This parser is intended to interpret the common firmware codes among all the different flavors"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
||||
|
@ -212,8 +211,9 @@ class FlavorParser:
|
|||
# G0 and G1 should be handled exactly the same.
|
||||
_gCode1 = _gCode0
|
||||
|
||||
## Home the head.
|
||||
def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
"""Home the head."""
|
||||
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
|
@ -221,21 +221,26 @@ class FlavorParser:
|
|||
position.f,
|
||||
position.e)
|
||||
|
||||
## Set the absolute positioning
|
||||
def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
"""Set the absolute positioning"""
|
||||
|
||||
self._is_absolute_positioning = True
|
||||
self._is_absolute_extrusion = True
|
||||
return position
|
||||
|
||||
## Set the relative positioning
|
||||
def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
"""Set the relative positioning"""
|
||||
|
||||
self._is_absolute_positioning = False
|
||||
self._is_absolute_extrusion = False
|
||||
return position
|
||||
|
||||
## Reset the current position to the values specified.
|
||||
# For example: G92 X10 will set the X to 10 without any physical motion.
|
||||
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
"""Reset the current position to the values specified.
|
||||
|
||||
For example: G92 X10 will set the X to 10 without any physical motion.
|
||||
"""
|
||||
|
||||
if params.e is not None:
|
||||
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
|
||||
self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
|
||||
|
@ -291,8 +296,9 @@ class FlavorParser:
|
|||
_type_keyword = ";TYPE:"
|
||||
_layer_keyword = ";LAYER:"
|
||||
|
||||
## For showing correct x, y offsets for each extruder
|
||||
def _extruderOffsets(self) -> Dict[int, List[float]]:
|
||||
"""For showing correct x, y offsets for each extruder"""
|
||||
|
||||
result = {}
|
||||
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
result[int(extruder.getMetaData().get("position", "0"))] = [
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
from . import FlavorParser
|
||||
|
||||
## This parser is intended to interpret the RepRap Firmware g-code flavor.
|
||||
|
||||
class RepRapFlavorParser(FlavorParser.FlavorParser):
|
||||
"""This parser is intended to interpret the RepRap Firmware g-code flavor."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
@ -17,16 +19,20 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
|
|||
# Set relative extrusion mode
|
||||
self._is_absolute_extrusion = False
|
||||
|
||||
## Set the absolute positioning
|
||||
# RepRapFlavor code G90 sets position of X, Y, Z to absolute
|
||||
# For absolute E, M82 is used
|
||||
def _gCode90(self, position, params, path):
|
||||
"""Set the absolute positioning
|
||||
|
||||
RepRapFlavor code G90 sets position of X, Y, Z to absolute
|
||||
For absolute E, M82 is used
|
||||
"""
|
||||
self._is_absolute_positioning = True
|
||||
return position
|
||||
|
||||
## Set the relative positioning
|
||||
# RepRapFlavor code G91 sets position of X, Y, Z to relative
|
||||
# For relative E, M83 is used
|
||||
def _gCode91(self, position, params, path):
|
||||
"""Set the relative positioning
|
||||
|
||||
RepRapFlavor code G91 sets position of X, Y, Z to relative
|
||||
For relative E, M83 is used
|
||||
"""
|
||||
self._is_absolute_positioning = False
|
||||
return position
|
|
@ -14,34 +14,40 @@ from cura.Machines.ContainerTree import ContainerTree
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Writes g-code to a file.
|
||||
#
|
||||
# While this poses as a mesh writer, what this really does is take the g-code
|
||||
# in the entire scene and write it to an output device. Since the g-code of a
|
||||
# single mesh isn't separable from the rest what with rafts and travel moves
|
||||
# and all, it doesn't make sense to write just a single mesh.
|
||||
#
|
||||
# So this plug-in takes the g-code that is stored in the root of the scene
|
||||
# node tree, adds a bit of extra information about the profiles and writes
|
||||
# that to the output device.
|
||||
class GCodeWriter(MeshWriter):
|
||||
## The file format version of the serialised g-code.
|
||||
#
|
||||
# It can only read settings with the same version as the version it was
|
||||
# written with. If the file format is changed in a way that breaks reverse
|
||||
# compatibility, increment this version number!
|
||||
version = 3
|
||||
|
||||
## Dictionary that defines how characters are escaped when embedded in
|
||||
# g-code.
|
||||
#
|
||||
# Note that the keys of this dictionary are regex strings. The values are
|
||||
# not.
|
||||
class GCodeWriter(MeshWriter):
|
||||
"""Writes g-code to a file.
|
||||
|
||||
While this poses as a mesh writer, what this really does is take the g-code
|
||||
in the entire scene and write it to an output device. Since the g-code of a
|
||||
single mesh isn't separable from the rest what with rafts and travel moves
|
||||
and all, it doesn't make sense to write just a single mesh.
|
||||
|
||||
So this plug-in takes the g-code that is stored in the root of the scene
|
||||
node tree, adds a bit of extra information about the profiles and writes
|
||||
that to the output device.
|
||||
"""
|
||||
|
||||
version = 3
|
||||
"""The file format version of the serialised g-code.
|
||||
|
||||
It can only read settings with the same version as the version it was
|
||||
written with. If the file format is changed in a way that breaks reverse
|
||||
compatibility, increment this version number!
|
||||
"""
|
||||
|
||||
escape_characters = {
|
||||
re.escape("\\"): "\\\\", # The escape character.
|
||||
re.escape("\n"): "\\n", # Newlines. They break off the comment.
|
||||
re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
|
||||
}
|
||||
"""Dictionary that defines how characters are escaped when embedded in
|
||||
|
||||
g-code.
|
||||
|
||||
Note that the keys of this dictionary are regex strings. The values are
|
||||
not.
|
||||
"""
|
||||
|
||||
_setting_keyword = ";SETTING_"
|
||||
|
||||
|
@ -50,17 +56,19 @@ class GCodeWriter(MeshWriter):
|
|||
|
||||
self._application = Application.getInstance()
|
||||
|
||||
## Writes the g-code for the entire scene to a stream.
|
||||
#
|
||||
# Note that even though the function accepts a collection of nodes, the
|
||||
# entire scene is always written to the file since it is not possible to
|
||||
# separate the g-code for just specific nodes.
|
||||
#
|
||||
# \param stream The stream to write the g-code to.
|
||||
# \param nodes This is ignored.
|
||||
# \param mode Additional information on how to format the g-code in the
|
||||
# file. This must always be text mode.
|
||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
|
||||
"""Writes the g-code for the entire scene to a stream.
|
||||
|
||||
Note that even though the function accepts a collection of nodes, the
|
||||
entire scene is always written to the file since it is not possible to
|
||||
separate the g-code for just specific nodes.
|
||||
|
||||
:param stream: The stream to write the g-code to.
|
||||
:param nodes: This is ignored.
|
||||
:param mode: Additional information on how to format the g-code in the
|
||||
file. This must always be text mode.
|
||||
"""
|
||||
|
||||
if mode != MeshWriter.OutputMode.TextMode:
|
||||
Logger.log("e", "GCodeWriter does not support non-text mode.")
|
||||
self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode."))
|
||||
|
@ -88,8 +96,9 @@ class GCodeWriter(MeshWriter):
|
|||
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
|
||||
return False
|
||||
|
||||
## Create a new container with container 2 as base and container 1 written over it.
|
||||
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
|
||||
"""Create a new container with container 2 as base and container 1 written over it."""
|
||||
|
||||
flat_container = InstanceContainer(instance_container2.getName())
|
||||
|
||||
# The metadata includes id, name and definition
|
||||
|
@ -106,15 +115,15 @@ class GCodeWriter(MeshWriter):
|
|||
|
||||
return flat_container
|
||||
|
||||
## Serialises a container stack to prepare it for writing at the end of the
|
||||
# g-code.
|
||||
#
|
||||
# The settings are serialised, and special characters (including newline)
|
||||
# are escaped.
|
||||
#
|
||||
# \param settings A container stack to serialise.
|
||||
# \return A serialised string of the settings.
|
||||
def _serialiseSettings(self, stack):
|
||||
"""Serialises a container stack to prepare it for writing at the end of the g-code.
|
||||
|
||||
The settings are serialised, and special characters (including newline)
|
||||
are escaped.
|
||||
|
||||
:param stack: A container stack to serialise.
|
||||
:return: A serialised string of the settings.
|
||||
"""
|
||||
container_registry = self._application.getContainerRegistry()
|
||||
|
||||
prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
||||
|
|
|
@ -16,58 +16,67 @@ from UM.Settings.InstanceContainer import InstanceContainer # The new profile t
|
|||
from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type to implement.
|
||||
|
||||
|
||||
## A plugin that reads profile data from legacy Cura versions.
|
||||
#
|
||||
# It reads a profile from an .ini file, and performs some translations on it.
|
||||
# Not all translations are correct, mind you, but it is a best effort.
|
||||
class LegacyProfileReader(ProfileReader):
|
||||
## Initialises the legacy profile reader.
|
||||
#
|
||||
# This does nothing since the only other function is basically stateless.
|
||||
"""A plugin that reads profile data from legacy Cura versions.
|
||||
|
||||
It reads a profile from an .ini file, and performs some translations on it.
|
||||
Not all translations are correct, mind you, but it is a best effort.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialises the legacy profile reader.
|
||||
|
||||
This does nothing since the only other function is basically stateless.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
## Prepares the default values of all legacy settings.
|
||||
#
|
||||
# These are loaded from the Dictionary of Doom.
|
||||
#
|
||||
# \param json The JSON file to load the default setting values from. This
|
||||
# should not be a URL but a pre-loaded JSON handle.
|
||||
# \return A dictionary of the default values of the legacy Cura version.
|
||||
def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]:
|
||||
"""Prepares the default values of all legacy settings.
|
||||
|
||||
These are loaded from the Dictionary of Doom.
|
||||
|
||||
:param json: The JSON file to load the default setting values from. This
|
||||
should not be a URL but a pre-loaded JSON handle.
|
||||
:return: A dictionary of the default values of the legacy Cura version.
|
||||
"""
|
||||
|
||||
defaults = {}
|
||||
if "defaults" in json:
|
||||
for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict.
|
||||
defaults[key] = json["defaults"][key]
|
||||
return defaults
|
||||
|
||||
## Prepares the local variables that can be used in evaluation of computing
|
||||
# new setting values from the old ones.
|
||||
#
|
||||
# This fills a dictionary with all settings from the legacy Cura version
|
||||
# and their values, so that they can be used in evaluating the new setting
|
||||
# values as Python code.
|
||||
#
|
||||
# \param config_parser The ConfigParser that finds the settings in the
|
||||
# legacy profile.
|
||||
# \param config_section The section in the profile where the settings
|
||||
# should be found.
|
||||
# \param defaults The default values for all settings in the legacy Cura.
|
||||
# \return A set of local variables, one for each setting in the legacy
|
||||
# profile.
|
||||
def prepareLocals(self, config_parser, config_section, defaults):
|
||||
"""Prepares the local variables that can be used in evaluation of computing
|
||||
|
||||
new setting values from the old ones.
|
||||
|
||||
This fills a dictionary with all settings from the legacy Cura version
|
||||
and their values, so that they can be used in evaluating the new setting
|
||||
values as Python code.
|
||||
|
||||
:param config_parser: The ConfigParser that finds the settings in the
|
||||
legacy profile.
|
||||
:param config_section: The section in the profile where the settings
|
||||
should be found.
|
||||
:param defaults: The default values for all settings in the legacy Cura.
|
||||
:return: A set of local variables, one for each setting in the legacy
|
||||
profile.
|
||||
"""
|
||||
copied_locals = defaults.copy() # Don't edit the original!
|
||||
for option in config_parser.options(config_section):
|
||||
copied_locals[option] = config_parser.get(config_section, option)
|
||||
return copied_locals
|
||||
|
||||
## Reads a legacy Cura profile from a file and returns it.
|
||||
#
|
||||
# \param file_name The file to read the legacy Cura profile from.
|
||||
# \return The legacy Cura profile that was in the file, if any. If the
|
||||
# file could not be read or didn't contain a valid profile, \code None
|
||||
# \endcode is returned.
|
||||
def read(self, file_name):
|
||||
"""Reads a legacy Cura profile from a file and returns it.
|
||||
|
||||
:param file_name: The file to read the legacy Cura profile from.
|
||||
:return: The legacy Cura profile that was in the file, if any. If the
|
||||
file could not be read or didn't contain a valid profile, None is returned.
|
||||
"""
|
||||
|
||||
if file_name.split(".")[-1] != "ini":
|
||||
return None
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
|
|
@ -13,7 +13,7 @@ import UM.PluginRegistry # To mock the plug-in registry out.
|
|||
import UM.Settings.ContainerRegistry # To mock the container registry out.
|
||||
import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function.
|
||||
|
||||
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
|
||||
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -126,9 +126,11 @@ test_prepareLocalsNoSectionErrorData = [
|
|||
)
|
||||
]
|
||||
|
||||
## Test cases where a key error is expected.
|
||||
|
||||
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData)
|
||||
def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults):
|
||||
"""Test cases where a key error is expected."""
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_dict(parser_data)
|
||||
|
||||
|
|
|
@ -23,9 +23,11 @@ if TYPE_CHECKING:
|
|||
catalog = UM.i18n.i18nCatalog("cura")
|
||||
|
||||
|
||||
## This action allows for certain settings that are "machine only") to be modified.
|
||||
# It automatically detects machine definitions that it knows how to change and attaches itself to those.
|
||||
class MachineSettingsAction(MachineAction):
|
||||
"""This action allows for certain settings that are "machine only") to be modified.
|
||||
|
||||
It automatically detects machine definitions that it knows how to change and attaches itself to those.
|
||||
"""
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
|
||||
self._qml_url = "MachineSettingsAction.qml"
|
||||
|
@ -56,9 +58,11 @@ class MachineSettingsAction(MachineAction):
|
|||
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
|
||||
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
|
||||
|
||||
## Triggered when the global container stack changes or when the g-code
|
||||
# flavour setting is changed.
|
||||
def _updateHasMaterialsInContainerTree(self) -> None:
|
||||
"""Triggered when the global container stack changes or when the g-code
|
||||
|
||||
flavour setting is changed.
|
||||
"""
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
|
|
@ -18,8 +18,8 @@ catalog = i18nCatalog("cura")
|
|||
|
||||
|
||||
class ModelChecker(QObject, Extension):
|
||||
## Signal that gets emitted when anything changed that we need to check.
|
||||
onChanged = pyqtSignal()
|
||||
"""Signal that gets emitted when anything changed that we need to check."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
@ -47,11 +47,13 @@ class ModelChecker(QObject, Extension):
|
|||
if not isinstance(args[0], Camera):
|
||||
self._change_timer.start()
|
||||
|
||||
## Called when plug-ins are initialized.
|
||||
#
|
||||
# This makes sure that we listen to changes of the material and that the
|
||||
# button is created that indicates warnings with the current set-up.
|
||||
def _pluginsInitialized(self):
|
||||
"""Called when plug-ins are initialized.
|
||||
|
||||
This makes sure that we listen to changes of the material and that the
|
||||
button is created that indicates warnings with the current set-up.
|
||||
"""
|
||||
|
||||
Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged)
|
||||
self._createView()
|
||||
|
||||
|
@ -106,8 +108,12 @@ class ModelChecker(QObject, Extension):
|
|||
if node.callDecoration("isSliceable"):
|
||||
yield node
|
||||
|
||||
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
|
||||
def _createView(self):
|
||||
"""Creates the view used by show popup.
|
||||
|
||||
The view is saved because of the fairly aggressive garbage collection.
|
||||
"""
|
||||
|
||||
Logger.log("d", "Creating model checker view.")
|
||||
|
||||
# Create the plugin dialog component
|
||||
|
|
|
@ -1,72 +1,72 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
## Stage for monitoring a 3D printing while it's printing.
|
||||
class MonitorStage(CuraStage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
self._onOutputDevicesChanged()
|
||||
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
class MonitorStage(CuraStage):
|
||||
"""Stage for monitoring a 3D printing while it's printing."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
self._onOutputDevicesChanged()
|
||||
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
|
|
|
@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit
|
|||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
## The per object setting visibility handler ensures that only setting
|
||||
# definitions that have a matching instance Container are returned as visible.
|
||||
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
|
||||
"""The per object setting visibility handler ensures that only setting
|
||||
|
||||
definitions that have a matching instance Container are returned as visible.
|
||||
"""
|
||||
def __init__(self, parent = None, *args, **kwargs):
|
||||
super().__init__(parent = parent, *args, **kwargs)
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance
|
|||
from UM.Event import Event
|
||||
|
||||
|
||||
## This tool allows the user to add & change settings per node in the scene.
|
||||
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||
class PerObjectSettingsTool(Tool):
|
||||
"""This tool allows the user to add & change settings per node in the scene.
|
||||
|
||||
The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._model = None
|
||||
|
@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool):
|
|||
except AttributeError:
|
||||
return ""
|
||||
|
||||
## Gets the active extruder of the currently selected object.
|
||||
#
|
||||
# \return The active extruder of the currently selected object.
|
||||
def getSelectedActiveExtruder(self):
|
||||
"""Gets the active extruder of the currently selected object.
|
||||
|
||||
:return: The active extruder of the currently selected object.
|
||||
"""
|
||||
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
return selected_object.callDecoration("getActiveExtruder")
|
||||
|
||||
## Changes the active extruder of the currently selected object.
|
||||
#
|
||||
# \param extruder_stack_id The ID of the extruder to print the currently
|
||||
# selected object with.
|
||||
def setSelectedActiveExtruder(self, extruder_stack_id):
|
||||
"""Changes the active extruder of the currently selected object.
|
||||
|
||||
:param extruder_stack_id: The ID of the extruder to print the currently
|
||||
selected object with.
|
||||
"""
|
||||
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
|
||||
|
||||
## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type
|
||||
def setMeshType(self, mesh_type: str) -> bool:
|
||||
"""Returns True when the mesh_type was changed, False when current mesh_type == mesh_type"""
|
||||
|
||||
old_mesh_type = self.getMeshType()
|
||||
if old_mesh_type == mesh_type:
|
||||
return False
|
||||
|
|
|
@ -27,9 +27,8 @@ if TYPE_CHECKING:
|
|||
from .Script import Script
|
||||
|
||||
|
||||
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
|
||||
# g-code files.
|
||||
class PostProcessingPlugin(QObject, Extension):
|
||||
"""Extension type plugin that enables pre-written scripts to post process g-code files."""
|
||||
def __init__(self, parent = None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
|
@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
except IndexError:
|
||||
return ""
|
||||
|
||||
## Execute all post-processing scripts on the gcode.
|
||||
def execute(self, output_device) -> None:
|
||||
"""Execute all post-processing scripts on the gcode."""
|
||||
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
# If the scene does not have a gcode, do nothing
|
||||
if not hasattr(scene, "gcode_dict"):
|
||||
|
@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.selectedIndexChanged.emit() #Ensure that settings are updated
|
||||
self._propertyChanged()
|
||||
|
||||
## Remove a script from the active script list by index.
|
||||
@pyqtSlot(int)
|
||||
def removeScriptByIndex(self, index: int) -> None:
|
||||
"""Remove a script from the active script list by index."""
|
||||
|
||||
self._script_list.pop(index)
|
||||
if len(self._script_list) - 1 < self._selected_script_index:
|
||||
self._selected_script_index = len(self._script_list) - 1
|
||||
|
@ -129,10 +130,12 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.selectedIndexChanged.emit() # Ensure that settings are updated
|
||||
self._propertyChanged()
|
||||
|
||||
## Load all scripts from all paths where scripts can be found.
|
||||
#
|
||||
# This should probably only be done on init.
|
||||
def loadAllScripts(self) -> None:
|
||||
"""Load all scripts from all paths where scripts can be found.
|
||||
|
||||
This should probably only be done on init.
|
||||
"""
|
||||
|
||||
if self._loaded_scripts: # Already loaded.
|
||||
return
|
||||
|
||||
|
@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
self.loadScripts(path)
|
||||
|
||||
## Load all scripts from provided path.
|
||||
# This should probably only be done on init.
|
||||
# \param path Path to check for scripts.
|
||||
def loadScripts(self, path: str) -> None:
|
||||
"""Load all scripts from provided path.
|
||||
|
||||
This should probably only be done on init.
|
||||
:param path: Path to check for scripts.
|
||||
"""
|
||||
|
||||
if ApplicationMetadata.IsEnterpriseVersion:
|
||||
# Delete all __pycache__ not in installation folder, as it may present a security risk.
|
||||
|
@ -173,8 +178,8 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
if not is_in_installation_path:
|
||||
TrustBasics.removeCached(path)
|
||||
|
||||
## Load all scripts in the scripts folders
|
||||
scripts = pkgutil.iter_modules(path = [path])
|
||||
"""Load all scripts in the scripts folders"""
|
||||
for loader, script_name, ispkg in scripts:
|
||||
# Iterate over all scripts.
|
||||
if script_name not in sys.modules:
|
||||
|
@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.scriptListChanged.emit()
|
||||
self._propertyChanged()
|
||||
|
||||
## When the global container stack is changed, swap out the list of active
|
||||
# scripts.
|
||||
def _onGlobalContainerStackChanged(self) -> None:
|
||||
"""When the global container stack is changed, swap out the list of active scripts."""
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
|
@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
# We do want to listen to other events.
|
||||
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
|
||||
def _createView(self) -> None:
|
||||
"""Creates the view used by show popup.
|
||||
|
||||
The view is saved because of the fairly aggressive garbage collection.
|
||||
"""
|
||||
|
||||
Logger.log("d", "Creating post processing plugin view.")
|
||||
|
||||
self.loadAllScripts()
|
||||
|
@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
# Create the save button component
|
||||
CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
|
||||
|
||||
## Show the (GUI) popup of the post processing plugin.
|
||||
def showPopup(self) -> None:
|
||||
"""Show the (GUI) popup of the post processing plugin."""
|
||||
|
||||
if self._view is None:
|
||||
self._createView()
|
||||
if self._view is None:
|
||||
|
@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
return
|
||||
self._view.show()
|
||||
|
||||
## Property changed: trigger re-slice
|
||||
# To do this we use the global container stack propertyChanged.
|
||||
# Re-slicing is necessary for setting changes in this plugin, because the changes
|
||||
# are applied only once per "fresh" gcode
|
||||
def _propertyChanged(self) -> None:
|
||||
"""Property changed: trigger re-slice
|
||||
|
||||
To do this we use the global container stack propertyChanged.
|
||||
Re-slicing is necessary for setting changes in this plugin, because the changes
|
||||
are applied only once per "fresh" gcode
|
||||
"""
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is not None:
|
||||
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
|
||||
|
|
|
@ -23,9 +23,10 @@ if TYPE_CHECKING:
|
|||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
|
||||
|
||||
## Base class for scripts. All scripts should inherit the script class.
|
||||
@signalemitter
|
||||
class Script:
|
||||
"""Base class for scripts. All scripts should inherit the script class."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._stack = None # type: Optional[ContainerStack]
|
||||
|
@ -78,13 +79,15 @@ class Script:
|
|||
if global_container_stack is not None:
|
||||
global_container_stack.propertyChanged.emit(key, property_name)
|
||||
|
||||
## Needs to return a dict that can be used to construct a settingcategory file.
|
||||
# See the example script for an example.
|
||||
# It follows the same style / guides as the Uranium settings.
|
||||
# Scripts can either override getSettingData directly, or use getSettingDataString
|
||||
# to return a string that will be parsed as json. The latter has the benefit over
|
||||
# returning a dict in that the order of settings is maintained.
|
||||
def getSettingData(self) -> Dict[str, Any]:
|
||||
"""Needs to return a dict that can be used to construct a settingcategory file.
|
||||
|
||||
See the example script for an example.
|
||||
It follows the same style / guides as the Uranium settings.
|
||||
Scripts can either override getSettingData directly, or use getSettingDataString
|
||||
to return a string that will be parsed as json. The latter has the benefit over
|
||||
returning a dict in that the order of settings is maintained.
|
||||
"""
|
||||
setting_data_as_string = self.getSettingDataString()
|
||||
setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
|
||||
return setting_data
|
||||
|
@ -104,15 +107,18 @@ class Script:
|
|||
return self._stack.getId()
|
||||
return None
|
||||
|
||||
## Convenience function that retrieves value of a setting from the stack.
|
||||
def getSettingValueByKey(self, key: str) -> Any:
|
||||
"""Convenience function that retrieves value of a setting from the stack."""
|
||||
|
||||
if self._stack is not None:
|
||||
return self._stack.getProperty(key, "value")
|
||||
return None
|
||||
|
||||
## Convenience function that finds the value in a line of g-code.
|
||||
# When requesting key = x from line "G1 X100" the value 100 is returned.
|
||||
def getValue(self, line: str, key: str, default = None) -> Any:
|
||||
"""Convenience function that finds the value in a line of g-code.
|
||||
|
||||
When requesting key = x from line "G1 X100" the value 100 is returned.
|
||||
"""
|
||||
if not key in line or (';' in line and line.find(key) > line.find(';')):
|
||||
return default
|
||||
sub_part = line[line.find(key) + 1:]
|
||||
|
@ -127,20 +133,23 @@ class Script:
|
|||
except ValueError: #Not a number at all.
|
||||
return default
|
||||
|
||||
## Convenience function to produce a line of g-code.
|
||||
#
|
||||
# You can put in an original g-code line and it'll re-use all the values
|
||||
# in that line.
|
||||
# All other keyword parameters are put in the result in g-code's format.
|
||||
# For instance, if you put ``G=1`` in the parameters, it will output
|
||||
# ``G1``. If you put ``G=1, X=100`` in the parameters, it will output
|
||||
# ``G1 X100``. The parameters G and M will always be put first. The
|
||||
# parameters T and S will be put second (or first if there is no G or M).
|
||||
# The rest of the parameters will be put in arbitrary order.
|
||||
# \param line The original g-code line that must be modified. If not
|
||||
# provided, an entirely new g-code line will be produced.
|
||||
# \return A line of g-code with the desired parameters filled in.
|
||||
def putValue(self, line: str = "", **kwargs) -> str:
|
||||
"""Convenience function to produce a line of g-code.
|
||||
|
||||
You can put in an original g-code line and it'll re-use all the values
|
||||
in that line.
|
||||
All other keyword parameters are put in the result in g-code's format.
|
||||
For instance, if you put ``G=1`` in the parameters, it will output
|
||||
``G1``. If you put ``G=1, X=100`` in the parameters, it will output
|
||||
``G1 X100``. The parameters G and M will always be put first. The
|
||||
parameters T and S will be put second (or first if there is no G or M).
|
||||
The rest of the parameters will be put in arbitrary order.
|
||||
|
||||
:param line: The original g-code line that must be modified. If not
|
||||
provided, an entirely new g-code line will be produced.
|
||||
:return: A line of g-code with the desired parameters filled in.
|
||||
"""
|
||||
|
||||
#Strip the comment.
|
||||
comment = ""
|
||||
if ";" in line:
|
||||
|
@ -179,7 +188,9 @@ class Script:
|
|||
|
||||
return result
|
||||
|
||||
## This is called when the script is executed.
|
||||
# It gets a list of g-code strings and needs to return a (modified) list.
|
||||
def execute(self, data: List[str]) -> List[str]:
|
||||
"""This is called when the script is executed.
|
||||
|
||||
It gets a list of g-code strings and needs to return a (modified) list.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
# Uses -
|
||||
# M163 - Set Mix Factor
|
||||
# M164 - Save Mix - saves to T2 as a unique mix
|
||||
|
||||
|
||||
import re #To perform the search and replace.
|
||||
from ..Script import Script
|
||||
|
||||
|
@ -127,7 +127,7 @@ class ColorMix(Script):
|
|||
firstMix = self.getSettingValueByKey("mix_start")
|
||||
secondMix = self.getSettingValueByKey("mix_finish")
|
||||
modelOfInterest = self.getSettingValueByKey("object_number")
|
||||
|
||||
|
||||
#get layer height
|
||||
layerHeight = 0
|
||||
for active_layer in data:
|
||||
|
@ -138,11 +138,11 @@ class ColorMix(Script):
|
|||
break
|
||||
if layerHeight != 0:
|
||||
break
|
||||
|
||||
|
||||
#default layerHeight if not found
|
||||
if layerHeight == 0:
|
||||
layerHeight = .2
|
||||
|
||||
|
||||
#get layers to use
|
||||
startLayer = 0
|
||||
endLayer = 0
|
||||
|
|
|
@ -56,7 +56,7 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
|||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
def execute(self, data):
|
||||
max_layer = 0
|
||||
if self.getSettingValueByKey("name") != "":
|
||||
|
@ -96,5 +96,5 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
|||
i += 1
|
||||
final_lines = "\n".join(lines)
|
||||
data[layer_index] = final_lines
|
||||
|
||||
|
||||
return data
|
||||
|
|
|
@ -63,10 +63,12 @@ class FilamentChange(Script):
|
|||
}
|
||||
}"""
|
||||
|
||||
## Inserts the filament change g-code at specific layer numbers.
|
||||
# \param data A list of layers of g-code.
|
||||
# \return A similar list, with filament change commands inserted.
|
||||
def execute(self, data: List[str]):
|
||||
"""Inserts the filament change g-code at specific layer numbers.
|
||||
|
||||
:param data: A list of layers of g-code.
|
||||
:return: A similar list, with filament change commands inserted.
|
||||
"""
|
||||
layer_nums = self.getSettingValueByKey("layer_number")
|
||||
initial_retract = self.getSettingValueByKey("initial_retract")
|
||||
later_retract = self.getSettingValueByKey("later_retract")
|
||||
|
|
|
@ -201,6 +201,7 @@ class PauseAtHeight(Script):
|
|||
## Get the X and Y values for a layer (will be used to get X and Y of the
|
||||
# layer after the pause).
|
||||
def getNextXY(self, layer: str) -> Tuple[float, float]:
|
||||
"""Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause)."""
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
|
||||
|
@ -209,10 +210,12 @@ class PauseAtHeight(Script):
|
|||
return x, y
|
||||
return 0, 0
|
||||
|
||||
## Inserts the pause commands.
|
||||
# \param data: List of layers.
|
||||
# \return New list of layers.
|
||||
def execute(self, data: List[str]) -> List[str]:
|
||||
"""Inserts the pause commands.
|
||||
|
||||
:param data: List of layers.
|
||||
:return: New list of layers.
|
||||
"""
|
||||
pause_at = self.getSettingValueByKey("pause_at")
|
||||
pause_height = self.getSettingValueByKey("pause_height")
|
||||
pause_layer = self.getSettingValueByKey("pause_layer")
|
||||
|
|
|
@ -5,8 +5,10 @@ import math
|
|||
|
||||
from ..Script import Script
|
||||
|
||||
## Continues retracting during all travel moves.
|
||||
|
||||
class RetractContinue(Script):
|
||||
"""Continues retracting during all travel moves."""
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Retract Continue",
|
||||
|
|
|
@ -5,11 +5,14 @@ import re #To perform the search and replace.
|
|||
|
||||
from ..Script import Script
|
||||
|
||||
## Performs a search-and-replace on all g-code.
|
||||
#
|
||||
# Due to technical limitations, the search can't cross the border between
|
||||
# layers.
|
||||
|
||||
class SearchAndReplace(Script):
|
||||
"""Performs a search-and-replace on all g-code.
|
||||
|
||||
Due to technical limitations, the search can't cross the border between
|
||||
layers.
|
||||
"""
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Search and Replace",
|
||||
|
|
|
@ -30,7 +30,7 @@ class UsePreviousProbeMeasurements(Script):
|
|||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
def execute(self, data):
|
||||
text = "M501 ;load bed level data\nM420 S1 ;enable bed leveling"
|
||||
if self.getSettingValueByKey("use_previous_measurements"):
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
## Stage for preparing model (slicing).
|
||||
class PrepareStage(CuraStage):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||
|
||||
def _engineCreated(self):
|
||||
menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
|
||||
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
class PrepareStage(CuraStage):
|
||||
"""Stage for preparing model (slicing)."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||
|
||||
def _engineCreated(self):
|
||||
menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
|
||||
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
|
@ -12,37 +12,45 @@ if TYPE_CHECKING:
|
|||
from UM.View.View import View
|
||||
|
||||
|
||||
## Displays a preview of what you're about to print.
|
||||
#
|
||||
# The Python component of this stage just loads PreviewMain.qml for display
|
||||
# when the stage is selected, and makes sure that it reverts to the previous
|
||||
# view when the previous stage is activated.
|
||||
class PreviewStage(CuraStage):
|
||||
"""Displays a preview of what you're about to print.
|
||||
|
||||
The Python component of this stage just loads PreviewMain.qml for display
|
||||
when the stage is selected, and makes sure that it reverts to the previous
|
||||
view when the previous stage is activated.
|
||||
"""
|
||||
|
||||
def __init__(self, application: QtApplication, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
self._application.engineCreatedSignal.connect(self._engineCreated)
|
||||
self._previously_active_view = None # type: Optional[View]
|
||||
|
||||
## When selecting the stage, remember which was the previous view so that
|
||||
# we can revert to that view when we go out of the stage later.
|
||||
def onStageSelected(self) -> None:
|
||||
"""When selecting the stage, remember which was the previous view so that
|
||||
|
||||
we can revert to that view when we go out of the stage later.
|
||||
"""
|
||||
self._previously_active_view = self._application.getController().getActiveView()
|
||||
|
||||
## Called when going to a different stage (away from the Preview Stage).
|
||||
#
|
||||
# When going to a different stage, the view should be reverted to what it
|
||||
# was before. Normally, that just reverts it to solid view.
|
||||
def onStageDeselected(self) -> None:
|
||||
"""Called when going to a different stage (away from the Preview Stage).
|
||||
|
||||
When going to a different stage, the view should be reverted to what it
|
||||
was before. Normally, that just reverts it to solid view.
|
||||
"""
|
||||
|
||||
if self._previously_active_view is not None:
|
||||
self._application.getController().setActiveView(self._previously_active_view.getPluginId())
|
||||
self._previously_active_view = None
|
||||
|
||||
## Delayed load of the QML files.
|
||||
#
|
||||
# We need to make sure that the QML engine is running before we can load
|
||||
# these.
|
||||
def _engineCreated(self) -> None:
|
||||
"""Delayed load of the QML files.
|
||||
|
||||
We need to make sure that the QML engine is running before we can load
|
||||
these.
|
||||
"""
|
||||
|
||||
plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml")
|
||||
|
|
|
@ -10,12 +10,14 @@ import glob
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
## Support for removable devices on Linux.
|
||||
#
|
||||
# TODO: This code uses the most basic interfaces for handling this.
|
||||
# We should instead use UDisks2 to handle mount/unmount and hotplugging events.
|
||||
#
|
||||
|
||||
class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
"""Support for removable devices on Linux.
|
||||
|
||||
TODO: This code uses the most basic interfaces for handling this.
|
||||
We should instead use UDisks2 to handle mount/unmount and hotplugging events.
|
||||
"""
|
||||
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
for volume in glob.glob("/media/*"):
|
||||
|
|
|
@ -9,8 +9,10 @@ import os
|
|||
|
||||
import plistlib
|
||||
|
||||
## Support for removable devices on Mac OSX
|
||||
|
||||
class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
"""Support for removable devices on Mac OSX"""
|
||||
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
|
||||
|
|
|
@ -28,17 +28,19 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
self._writing = False
|
||||
self._stream = None
|
||||
|
||||
## Request the specified nodes to be written to the removable drive.
|
||||
#
|
||||
# \param nodes A collection of scene nodes that should be written to the
|
||||
# removable drive.
|
||||
# \param file_name \type{string} A suggestion for the file name to write
|
||||
# to. If none is provided, a file name will be made from the names of the
|
||||
# meshes.
|
||||
# \param limit_mimetypes Should we limit the available MIME types to the
|
||||
# MIME types available to the currently active machine?
|
||||
#
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
||||
"""Request the specified nodes to be written to the removable drive.
|
||||
|
||||
:param nodes: A collection of scene nodes that should be written to the
|
||||
removable drive.
|
||||
:param file_name: :type{string} A suggestion for the file name to write to.
|
||||
If none is provided, a file name will be made from the names of the
|
||||
meshes.
|
||||
:param limit_mimetypes: Should we limit the available MIME types to the
|
||||
MIME types available to the currently active machine?
|
||||
|
||||
"""
|
||||
|
||||
filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do)
|
||||
if self._writing:
|
||||
raise OutputDeviceError.DeviceBusyError()
|
||||
|
@ -106,14 +108,14 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
|
||||
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
|
||||
|
||||
## Generate a file name automatically for the specified nodes to be saved
|
||||
# in.
|
||||
#
|
||||
# The name generated will be the name of one of the nodes. Which node that
|
||||
# is can not be guaranteed.
|
||||
#
|
||||
# \param nodes A collection of nodes for which to generate a file name.
|
||||
def _automaticFileName(self, nodes):
|
||||
"""Generate a file name automatically for the specified nodes to be saved in.
|
||||
|
||||
The name generated will be the name of one of the nodes. Which node that
|
||||
is can not be guaranteed.
|
||||
|
||||
:param nodes: A collection of nodes for which to generate a file name.
|
||||
"""
|
||||
for root in nodes:
|
||||
for child in BreadthFirstIterator(root):
|
||||
if child.getMeshData():
|
||||
|
|
|
@ -42,8 +42,9 @@ ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore
|
|||
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore
|
||||
|
||||
|
||||
## Removable drive support for windows
|
||||
class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
"""Removable drive support for windows"""
|
||||
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class SentryLogger(LogOutput):
|
|||
# processed and ready for sending.
|
||||
# Note that this only prepares them for sending. It only sends them when the user actually agrees to sending the
|
||||
# information.
|
||||
|
||||
|
||||
_levels = {
|
||||
"w": "warning",
|
||||
"i": "info",
|
||||
|
@ -32,11 +32,13 @@ class SentryLogger(LogOutput):
|
|||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._show_once = set() # type: Set[str]
|
||||
|
||||
## Log the message to the sentry hub as a breadcrumb
|
||||
# \param log_type "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once")
|
||||
# \param message String containing message to be logged
|
||||
|
||||
def log(self, log_type: str, message: str) -> None:
|
||||
"""Log the message to the sentry hub as a breadcrumb
|
||||
|
||||
:param log_type: "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once")
|
||||
:param message: String containing message to be logged
|
||||
"""
|
||||
level = self._translateLogType(log_type)
|
||||
message = CrashHandler.pruneSensitiveData(message)
|
||||
if level is None:
|
||||
|
|
|
@ -49,8 +49,9 @@ if TYPE_CHECKING:
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The preview layer view. It is used to display g-code paths.
|
||||
class SimulationView(CuraView):
|
||||
"""The preview layer view. It is used to display g-code paths."""
|
||||
|
||||
# Must match SimulationViewMenuComponent.qml
|
||||
LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
|
||||
LAYER_VIEW_TYPE_LINE_TYPE = 1
|
||||
|
@ -295,23 +296,28 @@ class SimulationView(CuraView):
|
|||
|
||||
self.currentPathNumChanged.emit()
|
||||
|
||||
## Set the layer view type
|
||||
#
|
||||
# \param layer_view_type integer as in SimulationView.qml and this class
|
||||
def setSimulationViewType(self, layer_view_type: int) -> None:
|
||||
"""Set the layer view type
|
||||
|
||||
:param layer_view_type: integer as in SimulationView.qml and this class
|
||||
"""
|
||||
|
||||
if layer_view_type != self._layer_view_type:
|
||||
self._layer_view_type = layer_view_type
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
## Return the layer view type, integer as in SimulationView.qml and this class
|
||||
def getSimulationViewType(self) -> int:
|
||||
"""Return the layer view type, integer as in SimulationView.qml and this class"""
|
||||
|
||||
return self._layer_view_type
|
||||
|
||||
## Set the extruder opacity
|
||||
#
|
||||
# \param extruder_nr 0..15
|
||||
# \param opacity 0.0 .. 1.0
|
||||
def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
|
||||
"""Set the extruder opacity
|
||||
|
||||
:param extruder_nr: 0..15
|
||||
:param opacity: 0.0 .. 1.0
|
||||
"""
|
||||
|
||||
if 0 <= extruder_nr <= 15:
|
||||
self._extruder_opacity[extruder_nr // 4][extruder_nr % 4] = opacity
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
@ -375,8 +381,8 @@ class SimulationView(CuraView):
|
|||
scene = self.getController().getScene()
|
||||
|
||||
self._old_max_layers = self._max_layers
|
||||
## Recalculate num max layers
|
||||
new_max_layers = -1
|
||||
"""Recalculate num max layers"""
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
|
@ -452,9 +458,11 @@ class SimulationView(CuraView):
|
|||
busyChanged = Signal()
|
||||
activityChanged = Signal()
|
||||
|
||||
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
|
||||
# as this caused some issues.
|
||||
def getProxy(self, engine, script_engine):
|
||||
"""Hackish way to ensure the proxy is already created
|
||||
|
||||
which ensures that the layerview.qml is already created as this caused some issues.
|
||||
"""
|
||||
if self._proxy is None:
|
||||
self._proxy = SimulationViewProxy(self)
|
||||
return self._proxy
|
||||
|
|
|
@ -26,10 +26,13 @@ if TYPE_CHECKING:
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## This Extension runs in the background and sends several bits of information to the Ultimaker servers.
|
||||
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
|
||||
# no model files are being sent (Just a SHA256 hash of the model).
|
||||
class SliceInfo(QObject, Extension):
|
||||
"""This Extension runs in the background and sends several bits of information to the Ultimaker servers.
|
||||
|
||||
The data is only sent when the user in question gave permission to do so. All data is anonymous and
|
||||
no model files are being sent (Just a SHA256 hash of the model).
|
||||
"""
|
||||
|
||||
info_url = "https://stats.ultimaker.com/api/cura"
|
||||
|
||||
def __init__(self, parent = None):
|
||||
|
@ -54,9 +57,11 @@ class SliceInfo(QObject, Extension):
|
|||
if self._more_info_dialog is None:
|
||||
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
|
||||
|
||||
## Perform action based on user input.
|
||||
# Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
|
||||
def messageActionTriggered(self, message_id, action_id):
|
||||
"""Perform action based on user input.
|
||||
|
||||
Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
|
||||
"""
|
||||
self._application.getPreferences().setValue("info/asked_send_slice_info", True)
|
||||
if action_id == "MoreInfo":
|
||||
self.showMoreInfoDialog()
|
||||
|
|
|
@ -33,9 +33,10 @@ import math
|
|||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Standard view for mesh models.
|
||||
|
||||
class SolidView(View):
|
||||
"""Standard view for mesh models."""
|
||||
|
||||
_show_xray_warning_preference = "view/show_xray_warning"
|
||||
|
||||
def __init__(self):
|
||||
|
|
|
@ -9,8 +9,12 @@ from PyQt5.QtCore import Qt, pyqtProperty
|
|||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
|
||||
class AuthorsModel(ListModel):
|
||||
"""Model that holds cura packages.
|
||||
|
||||
By setting the filter property the instances held by this model can be changed.
|
||||
"""
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
|
@ -67,9 +71,11 @@ class AuthorsModel(ListModel):
|
|||
filtered_items.sort(key = lambda k: k["name"])
|
||||
self.setItems(filtered_items)
|
||||
|
||||
## Set the filter of this model based on a string.
|
||||
# \param filter_dict \type{Dict} Dictionary to do the filtering by.
|
||||
def setFilter(self, filter_dict: Dict[str, str]) -> None:
|
||||
"""Set the filter of this model based on a string.
|
||||
|
||||
:param filter_dict: Dictionary to do the filtering by.
|
||||
"""
|
||||
if filter_dict != self._filter:
|
||||
self._filter = filter_dict
|
||||
self._update()
|
||||
|
|
|
@ -20,9 +20,9 @@ class CloudApiModel:
|
|||
cloud_api_version=cloud_api_version,
|
||||
)
|
||||
|
||||
## https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}
|
||||
@classmethod
|
||||
def userPackageUrl(cls, package_id: str) -> str:
|
||||
"""https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}"""
|
||||
|
||||
return (CloudApiModel.api_url_user_packages + "/{package_id}").format(
|
||||
package_id=package_id
|
||||
|
|
|
@ -8,9 +8,11 @@ from UM.Signal import Signal
|
|||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's
|
||||
# choices are emitted on the `packageMutations` Signal.
|
||||
class DiscrepanciesPresenter(QObject):
|
||||
"""Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's
|
||||
|
||||
choices are emitted on the `packageMutations` Signal.
|
||||
"""
|
||||
|
||||
def __init__(self, app: QtApplication) -> None:
|
||||
super().__init__(app)
|
||||
|
|
|
@ -16,9 +16,11 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
|||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Downloads a set of packages from the Ultimaker Cloud Marketplace
|
||||
# use download() exactly once: should not be used for multiple sets of downloads since this class contains state
|
||||
class DownloadPresenter:
|
||||
"""Downloads a set of packages from the Ultimaker Cloud Marketplace
|
||||
|
||||
use download() exactly once: should not be used for multiple sets of downloads since this class contains state
|
||||
"""
|
||||
|
||||
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
|
||||
|
||||
|
|
|
@ -43,10 +43,12 @@ class LicensePresenter(QObject):
|
|||
|
||||
self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml"
|
||||
|
||||
## Show a license dialog for multiple packages where users can read a license and accept or decline them
|
||||
# \param plugin_path: Root directory of the Toolbox plugin
|
||||
# \param packages: Dict[package id, file path]
|
||||
def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
|
||||
"""Show a license dialog for multiple packages where users can read a license and accept or decline them
|
||||
|
||||
:param plugin_path: Root directory of the Toolbox plugin
|
||||
:param packages: Dict[package id, file path]
|
||||
"""
|
||||
if self._presented:
|
||||
Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__)
|
||||
return
|
||||
|
|
|
@ -3,9 +3,11 @@ from UM.Message import Message
|
|||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## Presents a dialog telling the user that a restart is required to apply changes
|
||||
# Since we cannot restart Cura, the app is closed instead when the button is clicked
|
||||
class RestartApplicationPresenter:
|
||||
"""Presents a dialog telling the user that a restart is required to apply changes
|
||||
|
||||
Since we cannot restart Cura, the app is closed instead when the button is clicked
|
||||
"""
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
self._app = app
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
|
|
@ -16,20 +16,23 @@ from .RestartApplicationPresenter import RestartApplicationPresenter
|
|||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Orchestrates the synchronizing of packages from the user account to the installed packages
|
||||
# Example flow:
|
||||
# - CloudPackageChecker compares a list of packages the user `subscribed` to in their account
|
||||
# If there are `discrepancies` between the account and locally installed packages, they are emitted
|
||||
# - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
|
||||
# the user selected to be performed
|
||||
# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
||||
# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
|
||||
# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
|
||||
# be installed. It emits the `licenseAnswers` signal for accept or declines
|
||||
# - The CloudApiClient removes the declined packages from the account
|
||||
# - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files.
|
||||
# - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
|
||||
class SyncOrchestrator(Extension):
|
||||
"""Orchestrates the synchronizing of packages from the user account to the installed packages
|
||||
|
||||
Example flow:
|
||||
|
||||
- CloudPackageChecker compares a list of packages the user `subscribed` to in their account
|
||||
If there are `discrepancies` between the account and locally installed packages, they are emitted
|
||||
- DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
|
||||
the user selected to be performed
|
||||
- The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
||||
- The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
|
||||
- The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
|
||||
be installed. It emits the `licenseAnswers` signal for accept or declines
|
||||
- The CloudApiClient removes the declined packages from the account
|
||||
- The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files.
|
||||
- The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
|
||||
"""
|
||||
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
@ -63,10 +66,12 @@ class SyncOrchestrator(Extension):
|
|||
self._download_presenter.done.connect(self._onDownloadFinished)
|
||||
self._download_presenter.download(mutations)
|
||||
|
||||
## Called when a set of packages have finished downloading
|
||||
# \param success_items: Dict[package_id, Dict[str, str]]
|
||||
# \param error_items: List[package_id]
|
||||
def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
|
||||
"""Called when a set of packages have finished downloading
|
||||
|
||||
:param success_items:: Dict[package_id, Dict[str, str]]
|
||||
:param error_items:: List[package_id]
|
||||
"""
|
||||
if error_items:
|
||||
message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items)))
|
||||
self._showErrorMessage(message)
|
||||
|
@ -96,7 +101,8 @@ class SyncOrchestrator(Extension):
|
|||
if has_changes:
|
||||
self._restart_presenter.present()
|
||||
|
||||
## Logs an error and shows it to the user
|
||||
def _showErrorMessage(self, text: str):
|
||||
"""Logs an error and shows it to the user"""
|
||||
|
||||
Logger.error(text)
|
||||
Message(text, lifetime=0).show()
|
||||
|
|
|
@ -6,8 +6,9 @@ from PyQt5.QtCore import Qt
|
|||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
## Model that holds supported configurations (for material/quality packages).
|
||||
class ConfigsModel(ListModel):
|
||||
"""Model that holds supported configurations (for material/quality packages)."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
|
|
|
@ -12,8 +12,12 @@ from UM.Qt.ListModel import ListModel
|
|||
from .ConfigsModel import ConfigsModel
|
||||
|
||||
|
||||
## Model that holds Cura packages. By setting the filter property the instances held by this model can be changed.
|
||||
class PackagesModel(ListModel):
|
||||
"""Model that holds Cura packages.
|
||||
|
||||
By setting the filter property the instances held by this model can be changed.
|
||||
"""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
|
@ -131,9 +135,11 @@ class PackagesModel(ListModel):
|
|||
filtered_items.sort(key = lambda k: k["name"])
|
||||
self.setItems(filtered_items)
|
||||
|
||||
## Set the filter of this model based on a string.
|
||||
# \param filter_dict \type{Dict} Dictionary to do the filtering by.
|
||||
def setFilter(self, filter_dict: Dict[str, str]) -> None:
|
||||
"""Set the filter of this model based on a string.
|
||||
|
||||
:param filter_dict: Dictionary to do the filtering by.
|
||||
"""
|
||||
if filter_dict != self._filter:
|
||||
self._filter = filter_dict
|
||||
self._update()
|
||||
|
|
|
@ -37,10 +37,10 @@ try:
|
|||
except ImportError:
|
||||
CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT
|
||||
|
||||
# todo Remove license and download dialog, use SyncOrchestrator instead
|
||||
|
||||
## Provides a marketplace for users to download plugins an materials
|
||||
class Toolbox(QObject, Extension):
|
||||
"""Provides a marketplace for users to download plugins an materials"""
|
||||
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -135,8 +135,9 @@ class Toolbox(QObject, Extension):
|
|||
closeLicenseDialog = pyqtSignal()
|
||||
uninstallVariablesChanged = pyqtSignal()
|
||||
|
||||
## Go back to the start state (welcome screen or loading if no login required)
|
||||
def _restart(self):
|
||||
"""Go back to the start state (welcome screen or loading if no login required)"""
|
||||
|
||||
# For an Essentials build, login is mandatory
|
||||
if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion:
|
||||
self.setViewPage("welcome")
|
||||
|
@ -311,10 +312,13 @@ class Toolbox(QObject, Extension):
|
|||
self.restartRequiredChanged.emit()
|
||||
return package_id
|
||||
|
||||
## Check package usage and uninstall
|
||||
# If the package is in use, you'll get a confirmation dialog to set everything to default
|
||||
@pyqtSlot(str)
|
||||
def checkPackageUsageAndUninstall(self, package_id: str) -> None:
|
||||
"""Check package usage and uninstall
|
||||
|
||||
If the package is in use, you'll get a confirmation dialog to set everything to default
|
||||
"""
|
||||
|
||||
package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id)
|
||||
if package_used_materials or package_used_qualities:
|
||||
# Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall
|
||||
|
@ -352,10 +356,13 @@ class Toolbox(QObject, Extension):
|
|||
if self._confirm_reset_dialog is not None:
|
||||
self._confirm_reset_dialog.close()
|
||||
|
||||
## Uses "uninstall variables" to reset qualities and materials, then uninstall
|
||||
# It's used as an action on Confirm reset on Uninstall
|
||||
@pyqtSlot()
|
||||
def resetMaterialsQualitiesAndUninstall(self) -> None:
|
||||
"""Uses "uninstall variables" to reset qualities and materials, then uninstall
|
||||
|
||||
It's used as an action on Confirm reset on Uninstall
|
||||
"""
|
||||
|
||||
application = CuraApplication.getInstance()
|
||||
machine_manager = application.getMachineManager()
|
||||
container_tree = ContainerTree.getInstance()
|
||||
|
@ -418,8 +425,9 @@ class Toolbox(QObject, Extension):
|
|||
self._restart_required = True
|
||||
self.restartRequiredChanged.emit()
|
||||
|
||||
## Actual update packages that are in self._to_update
|
||||
def _update(self) -> None:
|
||||
"""Actual update packages that are in self._to_update"""
|
||||
|
||||
if self._to_update:
|
||||
plugin_id = self._to_update.pop(0)
|
||||
remote_package = self.getRemotePackage(plugin_id)
|
||||
|
@ -433,9 +441,10 @@ class Toolbox(QObject, Extension):
|
|||
if self._to_update:
|
||||
self._application.callLater(self._update)
|
||||
|
||||
## Update a plugin by plugin_id
|
||||
@pyqtSlot(str)
|
||||
def update(self, plugin_id: str) -> None:
|
||||
"""Update a plugin by plugin_id"""
|
||||
|
||||
self._to_update.append(plugin_id)
|
||||
self._application.callLater(self._update)
|
||||
|
||||
|
@ -714,9 +723,10 @@ class Toolbox(QObject, Extension):
|
|||
self._active_package = package
|
||||
self.activePackageChanged.emit()
|
||||
|
||||
## The active package is the package that is currently being downloaded
|
||||
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
|
||||
def activePackage(self) -> Optional[QObject]:
|
||||
"""The active package is the package that is currently being downloaded"""
|
||||
|
||||
return self._active_package
|
||||
|
||||
def setViewCategory(self, category: str = "plugin") -> None:
|
||||
|
@ -724,7 +734,7 @@ class Toolbox(QObject, Extension):
|
|||
self._view_category = category
|
||||
self.viewChanged.emit()
|
||||
|
||||
## Function explicitly defined so that it can be called through the callExtensionsMethod
|
||||
# Function explicitly defined so that it can be called through the callExtensionsMethod
|
||||
# which cannot receive arguments.
|
||||
def setViewCategoryToMaterials(self) -> None:
|
||||
self.setViewCategory("material")
|
||||
|
|
|
@ -22,8 +22,10 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator # Adde
|
|||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
## Class that leverages Trimesh to import files.
|
||||
|
||||
class TrimeshReader(MeshReader):
|
||||
"""Class that leverages Trimesh to import files."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
@ -79,11 +81,13 @@ class TrimeshReader(MeshReader):
|
|||
)
|
||||
)
|
||||
|
||||
## Reads a file using Trimesh.
|
||||
# \param file_name The file path. This is assumed to be one of the file
|
||||
# types that Trimesh can read. It will not be checked again.
|
||||
# \return A scene node that contains the file's contents.
|
||||
def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]:
|
||||
"""Reads a file using Trimesh.
|
||||
|
||||
:param file_name: The file path. This is assumed to be one of the file
|
||||
types that Trimesh can read. It will not be checked again.
|
||||
:return: A scene node that contains the file's contents.
|
||||
"""
|
||||
# CURA-6739
|
||||
# GLTF files are essentially JSON files. If you directly give a file name to trimesh.load(), it will
|
||||
# try to figure out the format, but for GLTF, it loads it as a binary file with flags "rb", and the json.load()
|
||||
|
@ -130,13 +134,14 @@ class TrimeshReader(MeshReader):
|
|||
node.setParent(group_node)
|
||||
return group_node
|
||||
|
||||
## Converts a Trimesh to Uranium's MeshData.
|
||||
# \param tri_node A Trimesh containing the contents of a file that was
|
||||
# just read.
|
||||
# \param file_name The full original filename used to watch for changes
|
||||
# \return Mesh data from the Trimesh in a way that Uranium can understand
|
||||
# it.
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData:
|
||||
"""Converts a Trimesh to Uranium's MeshData.
|
||||
|
||||
:param tri_node: A Trimesh containing the contents of a file that was just read.
|
||||
:param file_name: The full original filename used to watch for changes
|
||||
:return: Mesh data from the Trimesh in a way that Uranium can understand it.
|
||||
"""
|
||||
|
||||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
|
|
|
@ -24,13 +24,15 @@ from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
|||
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
||||
from ..Models.Http.CloudPrintResponse import CloudPrintResponse
|
||||
|
||||
## The generic type variable used to document the methods below.
|
||||
CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel)
|
||||
"""The generic type variable used to document the methods below."""
|
||||
|
||||
|
||||
## The cloud API client is responsible for handling the requests and responses from the cloud.
|
||||
# Each method should only handle models instead of exposing Any HTTP details.
|
||||
class CloudApiClient:
|
||||
"""The cloud API client is responsible for handling the requests and responses from the cloud.
|
||||
|
||||
Each method should only handle models instead of exposing Any HTTP details.
|
||||
"""
|
||||
|
||||
# The cloud URL to use for this remote cluster.
|
||||
ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
|
||||
|
@ -42,10 +44,13 @@ class CloudApiClient:
|
|||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]]
|
||||
|
||||
## Initializes a new cloud API client.
|
||||
# \param account: The user's account object
|
||||
# \param on_error: The callback to be called whenever we receive errors from the server.
|
||||
def __init__(self, app: CuraApplication, on_error: Callable[[List[CloudError]], None]) -> None:
|
||||
"""Initializes a new cloud API client.
|
||||
|
||||
:param app:
|
||||
:param account: The user's account object
|
||||
:param on_error: The callback to be called whenever we receive errors from the server.
|
||||
"""
|
||||
super().__init__()
|
||||
self._app = app
|
||||
self._account = app.getCuraAPI().account
|
||||
|
@ -54,14 +59,18 @@ class CloudApiClient:
|
|||
self._on_error = on_error
|
||||
self._upload = None # type: Optional[ToolPathUploader]
|
||||
|
||||
## Gets the account used for the API.
|
||||
@property
|
||||
def account(self) -> Account:
|
||||
"""Gets the account used for the API."""
|
||||
|
||||
return self._account
|
||||
|
||||
## Retrieves all the clusters for the user that is currently logged in.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
|
||||
"""Retrieves all the clusters for the user that is currently logged in.
|
||||
|
||||
:param on_finished: The function to be called after the result is parsed.
|
||||
"""
|
||||
|
||||
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
|
||||
self._http.get(url,
|
||||
scope = self._scope,
|
||||
|
@ -69,21 +78,28 @@ class CloudApiClient:
|
|||
error_callback = failed,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
## Retrieves the status of the given cluster.
|
||||
# \param cluster_id: The ID of the cluster.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
|
||||
"""Retrieves the status of the given cluster.
|
||||
|
||||
:param cluster_id: The ID of the cluster.
|
||||
:param on_finished: The function to be called after the result is parsed.
|
||||
"""
|
||||
|
||||
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
|
||||
self._http.get(url,
|
||||
scope = self._scope,
|
||||
callback = self._parseCallback(on_finished, CloudClusterStatus),
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
## Requests the cloud to register the upload of a print job mesh.
|
||||
# \param request: The request object.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def requestUpload(self, request: CloudPrintJobUploadRequest,
|
||||
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
|
||||
|
||||
"""Requests the cloud to register the upload of a print job mesh.
|
||||
|
||||
:param request: The request object.
|
||||
:param on_finished: The function to be called after the result is parsed.
|
||||
"""
|
||||
|
||||
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
||||
data = json.dumps({"data": request.toDict()}).encode()
|
||||
|
||||
|
@ -93,14 +109,17 @@ class CloudApiClient:
|
|||
callback = self._parseCallback(on_finished, CloudPrintJobResponse),
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
## Uploads a print job tool path to the cloud.
|
||||
# \param print_job: The object received after requesting an upload with `self.requestUpload`.
|
||||
# \param mesh: The tool path data to be uploaded.
|
||||
# \param on_finished: The function to be called after the upload is successful.
|
||||
# \param on_progress: A function to be called during upload progress. It receives a percentage (0-100).
|
||||
# \param on_error: A function to be called if the upload fails.
|
||||
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
|
||||
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
|
||||
"""Uploads a print job tool path to the cloud.
|
||||
|
||||
:param print_job: The object received after requesting an upload with `self.requestUpload`.
|
||||
:param mesh: The tool path data to be uploaded.
|
||||
:param on_finished: The function to be called after the upload is successful.
|
||||
:param on_progress: A function to be called during upload progress. It receives a percentage (0-100).
|
||||
:param on_error: A function to be called if the upload fails.
|
||||
"""
|
||||
|
||||
self._upload = ToolPathUploader(self._http, print_job, mesh, on_finished, on_progress, on_error)
|
||||
self._upload.start()
|
||||
|
||||
|
@ -116,12 +135,16 @@ class CloudApiClient:
|
|||
callback = self._parseCallback(on_finished, CloudPrintResponse),
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
## Send a print job action to the cluster for the given print job.
|
||||
# \param cluster_id: The ID of the cluster.
|
||||
# \param cluster_job_id: The ID of the print job within the cluster.
|
||||
# \param action: The name of the action to execute.
|
||||
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,
|
||||
data: Optional[Dict[str, Any]] = None) -> None:
|
||||
|
||||
"""Send a print job action to the cluster for the given print job.
|
||||
|
||||
:param cluster_id: The ID of the cluster.
|
||||
:param cluster_job_id: The ID of the print job within the cluster.
|
||||
:param action: The name of the action to execute.
|
||||
"""
|
||||
|
||||
body = json.dumps({"data": data}).encode() if data else b""
|
||||
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
|
||||
self._http.post(url,
|
||||
|
@ -129,10 +152,13 @@ class CloudApiClient:
|
|||
data = body,
|
||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||
|
||||
## We override _createEmptyRequest in order to add the user credentials.
|
||||
# \param url: The URL to request
|
||||
# \param content_type: The type of the body contents.
|
||||
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
|
||||
"""We override _createEmptyRequest in order to add the user credentials.
|
||||
|
||||
:param url: The URL to request
|
||||
:param content_type: The type of the body contents.
|
||||
"""
|
||||
|
||||
request = QNetworkRequest(QUrl(path))
|
||||
if content_type:
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||
|
@ -141,11 +167,14 @@ class CloudApiClient:
|
|||
request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode())
|
||||
return request
|
||||
|
||||
## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
|
||||
# \param reply: The reply from the server.
|
||||
# \return A tuple with a status code and a dictionary.
|
||||
@staticmethod
|
||||
def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]:
|
||||
"""Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
|
||||
|
||||
:param reply: The reply from the server.
|
||||
:return: A tuple with a status code and a dictionary.
|
||||
"""
|
||||
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
try:
|
||||
response = bytes(reply.readAll()).decode()
|
||||
|
@ -156,14 +185,15 @@ class CloudApiClient:
|
|||
Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
|
||||
return status_code, {"errors": [error.toDict()]}
|
||||
|
||||
## Parses the given models and calls the correct callback depending on the result.
|
||||
# \param response: The response from the server, after being converted to a dict.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
# \param model_class: The type of the model to convert the response to. It may either be a single record or a list.
|
||||
def _parseModels(self, response: Dict[str, Any],
|
||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model_class: Type[CloudApiClientModel]) -> None:
|
||||
def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None:
|
||||
"""Parses the given models and calls the correct callback depending on the result.
|
||||
|
||||
:param response: The response from the server, after being converted to a dict.
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
:param model_class: The type of the model to convert the response to. It may either be a single record or a list.
|
||||
"""
|
||||
|
||||
if "data" in response:
|
||||
data = response["data"]
|
||||
if isinstance(data, list):
|
||||
|
@ -179,18 +209,23 @@ class CloudApiClient:
|
|||
else:
|
||||
Logger.log("e", "Cannot find data or errors in the cloud response: %s", response)
|
||||
|
||||
## Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
# The callback is added to the 'finished' signal of the reply.
|
||||
# \param reply: The reply that should be listened to.
|
||||
# \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
|
||||
# a list or a single item.
|
||||
# \param model: The type of the model to convert the response to.
|
||||
def _parseCallback(self,
|
||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model: Type[CloudApiClientModel],
|
||||
on_error: Optional[Callable] = None) -> Callable[[QNetworkReply], None]:
|
||||
|
||||
"""Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
|
||||
The callback is added to the 'finished' signal of the reply.
|
||||
:param reply: The reply that should be listened to.
|
||||
:param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
|
||||
a list or a single item.
|
||||
:param model: The type of the model to convert the response to.
|
||||
"""
|
||||
|
||||
def parse(reply: QNetworkReply) -> None:
|
||||
|
||||
self._anti_gc_callbacks.remove(parse)
|
||||
|
||||
# Don't try to parse the reply if we didn't get one
|
||||
|
|
|
@ -35,11 +35,13 @@ from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The cloud output device is a network output device that works remotely but has limited functionality.
|
||||
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||
# As such, those methods have been implemented here.
|
||||
# Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||
class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||
"""The cloud output device is a network output device that works remotely but has limited functionality.
|
||||
|
||||
Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||
As such, those methods have been implemented here.
|
||||
Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||
"""
|
||||
|
||||
# The interval with which the remote cluster is checked.
|
||||
# We can do this relatively often as this API call is quite fast.
|
||||
|
@ -56,11 +58,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
# Therefore we create a private signal used to trigger the printersChanged signal.
|
||||
_cloudClusterPrintersChanged = pyqtSignal()
|
||||
|
||||
## Creates a new cloud output device
|
||||
# \param api_client: The client that will run the API calls
|
||||
# \param cluster: The device response received from the cloud API.
|
||||
# \param parent: The optional parent of this output device.
|
||||
def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
|
||||
"""Creates a new cloud output device
|
||||
|
||||
:param api_client: The client that will run the API calls
|
||||
:param cluster: The device response received from the cloud API.
|
||||
:param parent: The optional parent of this output device.
|
||||
"""
|
||||
|
||||
# The following properties are expected on each networked output device.
|
||||
# Because the cloud connection does not off all of these, we manually construct this version here.
|
||||
|
@ -99,8 +103,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._tool_path = None # type: Optional[bytes]
|
||||
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
|
||||
|
||||
## Connects this device.
|
||||
def connect(self) -> None:
|
||||
"""Connects this device."""
|
||||
|
||||
if self.isConnected():
|
||||
return
|
||||
super().connect()
|
||||
|
@ -108,21 +113,24 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
|
||||
self._update()
|
||||
|
||||
## Disconnects the device
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnects the device"""
|
||||
|
||||
if not self.isConnected():
|
||||
return
|
||||
super().disconnect()
|
||||
Logger.log("i", "Disconnected from cluster %s", self.key)
|
||||
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
|
||||
|
||||
## Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.
|
||||
def _onBackendStateChange(self, _: BackendState) -> None:
|
||||
"""Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices."""
|
||||
|
||||
self._tool_path = None
|
||||
self._uploaded_print_job = None
|
||||
|
||||
## Checks whether the given network key is found in the cloud's host name
|
||||
def matchesNetworkKey(self, network_key: str) -> bool:
|
||||
"""Checks whether the given network key is found in the cloud's host name"""
|
||||
|
||||
# Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
||||
# the host name should then be "ultimakersystem-aabbccdd0011"
|
||||
if network_key.startswith(str(self.clusterData.host_name or "")):
|
||||
|
@ -133,15 +141,17 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
return True
|
||||
return False
|
||||
|
||||
## Set all the interface elements and texts for this output device.
|
||||
def _setInterfaceElements(self) -> None:
|
||||
"""Set all the interface elements and texts for this output device."""
|
||||
|
||||
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'.
|
||||
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
|
||||
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
|
||||
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
|
||||
|
||||
## Called when the network data should be updated.
|
||||
def _update(self) -> None:
|
||||
"""Called when the network data should be updated."""
|
||||
|
||||
super()._update()
|
||||
if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
|
||||
return # avoid calling the cloud too often
|
||||
|
@ -153,9 +163,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
else:
|
||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||
|
||||
## Method called when HTTP request to status endpoint is finished.
|
||||
# Contains both printers and print jobs statuses in a single response.
|
||||
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
|
||||
"""Method called when HTTP request to status endpoint is finished.
|
||||
|
||||
Contains both printers and print jobs statuses in a single response.
|
||||
"""
|
||||
self._responseReceived()
|
||||
if status.printers != self._received_printers:
|
||||
self._received_printers = status.printers
|
||||
|
@ -164,10 +176,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._received_print_jobs = status.print_jobs
|
||||
self._updatePrintJobs(status.print_jobs)
|
||||
|
||||
## Called when Cura requests an output device to receive a (G-code) file.
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
|
||||
"""Called when Cura requests an output device to receive a (G-code) file."""
|
||||
|
||||
# Show an error message if we're already sending a job.
|
||||
if self._progress.visible:
|
||||
PrintJobUploadBlockedMessage().show()
|
||||
|
@ -187,9 +200,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
job.finished.connect(self._onPrintJobCreated)
|
||||
job.start()
|
||||
|
||||
## Handler for when the print job was created locally.
|
||||
# It can now be sent over the cloud.
|
||||
def _onPrintJobCreated(self, job: ExportFileJob) -> None:
|
||||
"""Handler for when the print job was created locally.
|
||||
|
||||
It can now be sent over the cloud.
|
||||
"""
|
||||
output = job.getOutput()
|
||||
self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again
|
||||
file_name = job.getFileName()
|
||||
|
@ -200,9 +215,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
)
|
||||
self._api.requestUpload(request, self._uploadPrintJob)
|
||||
|
||||
## Uploads the mesh when the print job was registered with the cloud API.
|
||||
# \param job_response: The response received from the cloud API.
|
||||
def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None:
|
||||
"""Uploads the mesh when the print job was registered with the cloud API.
|
||||
|
||||
:param job_response: The response received from the cloud API.
|
||||
"""
|
||||
if not self._tool_path:
|
||||
return self._onUploadError()
|
||||
self._progress.show()
|
||||
|
@ -210,38 +227,45 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
|
||||
self._onUploadError)
|
||||
|
||||
## Requests the print to be sent to the printer when we finished uploading the mesh.
|
||||
def _onPrintJobUploaded(self) -> None:
|
||||
"""Requests the print to be sent to the printer when we finished uploading the mesh."""
|
||||
|
||||
self._progress.update(100)
|
||||
print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
|
||||
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
|
||||
|
||||
## Shows a message when the upload has succeeded
|
||||
# \param response: The response from the cloud API.
|
||||
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
||||
"""Shows a message when the upload has succeeded
|
||||
|
||||
:param response: The response from the cloud API.
|
||||
"""
|
||||
self._progress.hide()
|
||||
PrintJobUploadSuccessMessage().show()
|
||||
self.writeFinished.emit()
|
||||
|
||||
## Displays the given message if uploading the mesh has failed
|
||||
# \param message: The message to display.
|
||||
def _onUploadError(self, message: str = None) -> None:
|
||||
"""Displays the given message if uploading the mesh has failed
|
||||
|
||||
:param message: The message to display.
|
||||
"""
|
||||
self._progress.hide()
|
||||
self._uploaded_print_job = None
|
||||
PrintJobUploadErrorMessage(message).show()
|
||||
self.writeError.emit()
|
||||
|
||||
## Whether the printer that this output device represents supports print job actions via the cloud.
|
||||
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
|
||||
def supportsPrintJobActions(self) -> bool:
|
||||
"""Whether the printer that this output device represents supports print job actions via the cloud."""
|
||||
|
||||
if not self._printers:
|
||||
return False
|
||||
version_number = self.printers[0].firmwareVersion.split(".")
|
||||
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
|
||||
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
|
||||
|
||||
## Set the remote print job state.
|
||||
def setJobState(self, print_job_uuid: str, state: str) -> None:
|
||||
"""Set the remote print job state."""
|
||||
|
||||
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state)
|
||||
|
||||
@pyqtSlot(str, name="sendJobToTop")
|
||||
|
@ -265,18 +289,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
def openPrinterControlPanel(self) -> None:
|
||||
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl))
|
||||
|
||||
## Gets the cluster response from which this device was created.
|
||||
@property
|
||||
def clusterData(self) -> CloudClusterResponse:
|
||||
"""Gets the cluster response from which this device was created."""
|
||||
|
||||
return self._cluster
|
||||
|
||||
## Updates the cluster data from the cloud.
|
||||
@clusterData.setter
|
||||
def clusterData(self, value: CloudClusterResponse) -> None:
|
||||
"""Updates the cluster data from the cloud."""
|
||||
|
||||
self._cluster = value
|
||||
|
||||
## Gets the URL on which to monitor the cluster via the cloud.
|
||||
@property
|
||||
def clusterCloudUrl(self) -> str:
|
||||
"""Gets the URL on which to monitor the cluster via the cloud."""
|
||||
|
||||
root_url_prefix = "-staging" if self._account.is_staging else ""
|
||||
return "https://mycloud{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id)
|
||||
|
|
|
@ -22,7 +22,7 @@ from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
|||
|
||||
class CloudOutputDeviceManager:
|
||||
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
||||
|
||||
|
||||
Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
||||
API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
||||
"""
|
||||
|
|
|
@ -10,8 +10,9 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
|||
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||
|
||||
|
||||
## Class responsible for uploading meshes to the cloud in separate requests.
|
||||
class ToolPathUploader:
|
||||
"""Class responsible for uploading meshes to the cloud in separate requests."""
|
||||
|
||||
|
||||
# The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES
|
||||
MAX_RETRIES = 10
|
||||
|
@ -22,16 +23,19 @@ class ToolPathUploader:
|
|||
# The amount of bytes to send per request
|
||||
BYTES_PER_REQUEST = 256 * 1024
|
||||
|
||||
## Creates a mesh upload object.
|
||||
# \param http: The HttpRequestManager that will handle the HTTP requests.
|
||||
# \param print_job: The print job response that was returned by the cloud after registering the upload.
|
||||
# \param data: The mesh bytes to be uploaded.
|
||||
# \param on_finished: The method to be called when done.
|
||||
# \param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
|
||||
# \param on_error: The method to be called when an error occurs.
|
||||
def __init__(self, http: HttpRequestManager, print_job: CloudPrintJobResponse, data: bytes,
|
||||
on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]
|
||||
) -> None:
|
||||
"""Creates a mesh upload object.
|
||||
|
||||
:param manager: The network access manager that will handle the HTTP requests.
|
||||
:param print_job: The print job response that was returned by the cloud after registering the upload.
|
||||
:param data: The mesh bytes to be uploaded.
|
||||
:param on_finished: The method to be called when done.
|
||||
:param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
|
||||
:param on_error: The method to be called when an error occurs.
|
||||
"""
|
||||
|
||||
self._http = http
|
||||
self._print_job = print_job
|
||||
self._data = data
|
||||
|
@ -44,19 +48,23 @@ class ToolPathUploader:
|
|||
self._retries = 0
|
||||
self._finished = False
|
||||
|
||||
## Returns the print job for which this object was created.
|
||||
@property
|
||||
def printJob(self):
|
||||
"""Returns the print job for which this object was created."""
|
||||
|
||||
return self._print_job
|
||||
|
||||
## Determines the bytes that should be uploaded next.
|
||||
# \return: A tuple with the first and the last byte to upload.
|
||||
def _chunkRange(self) -> Tuple[int, int]:
|
||||
"""Determines the bytes that should be uploaded next.
|
||||
|
||||
:return: A tuple with the first and the last byte to upload.
|
||||
"""
|
||||
last_byte = min(len(self._data), self._sent_bytes + self.BYTES_PER_REQUEST)
|
||||
return self._sent_bytes, last_byte
|
||||
|
||||
## Starts uploading the mesh.
|
||||
def start(self) -> None:
|
||||
"""Starts uploading the mesh."""
|
||||
|
||||
if self._finished:
|
||||
# reset state.
|
||||
self._sent_bytes = 0
|
||||
|
@ -64,13 +72,15 @@ class ToolPathUploader:
|
|||
self._finished = False
|
||||
self._uploadChunk()
|
||||
|
||||
## Stops uploading the mesh, marking it as finished.
|
||||
def stop(self):
|
||||
"""Stops uploading the mesh, marking it as finished."""
|
||||
|
||||
Logger.log("i", "Stopped uploading")
|
||||
self._finished = True
|
||||
|
||||
## Uploads a chunk of the mesh to the cloud.
|
||||
def _uploadChunk(self) -> None:
|
||||
"""Uploads a chunk of the mesh to the cloud."""
|
||||
|
||||
if self._finished:
|
||||
raise ValueError("The upload is already finished")
|
||||
|
||||
|
@ -93,10 +103,12 @@ class ToolPathUploader:
|
|||
upload_progress_callback = self._progressCallback
|
||||
)
|
||||
|
||||
## Handles an update to the upload progress
|
||||
# \param bytes_sent: The amount of bytes sent in the current request.
|
||||
# \param bytes_total: The amount of bytes to send in the current request.
|
||||
def _progressCallback(self, bytes_sent: int, bytes_total: int) -> None:
|
||||
"""Handles an update to the upload progress
|
||||
|
||||
:param bytes_sent: The amount of bytes sent in the current request.
|
||||
:param bytes_total: The amount of bytes to send in the current request.
|
||||
"""
|
||||
Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total)
|
||||
if bytes_total:
|
||||
total_sent = self._sent_bytes + bytes_sent
|
||||
|
@ -104,13 +116,16 @@ class ToolPathUploader:
|
|||
|
||||
## Handles an error uploading.
|
||||
def _errorCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||
"""Handles an error uploading."""
|
||||
|
||||
body = bytes(reply.readAll()).decode()
|
||||
Logger.log("e", "Received error while uploading: %s", body)
|
||||
self.stop()
|
||||
self._on_error()
|
||||
|
||||
## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
|
||||
def _finishedCallback(self, reply: QNetworkReply) -> None:
|
||||
"""Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed."""
|
||||
|
||||
Logger.log("i", "Finished callback %s %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
|
||||
|
||||
|
@ -135,8 +150,9 @@ class ToolPathUploader:
|
|||
[bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode())
|
||||
self._chunkUploaded()
|
||||
|
||||
## Handles a chunk of data being uploaded, starting the next chunk if needed.
|
||||
def _chunkUploaded(self) -> None:
|
||||
"""Handles a chunk of data being uploaded, starting the next chunk if needed."""
|
||||
|
||||
# We got a successful response. Let's start the next chunk or report the upload is finished.
|
||||
first_byte, last_byte = self._chunkRange()
|
||||
self._sent_bytes += last_byte - first_byte
|
||||
|
|
|
@ -9,8 +9,8 @@ from cura.CuraApplication import CuraApplication
|
|||
from .MeshFormatHandler import MeshFormatHandler
|
||||
|
||||
|
||||
## Job that exports the build plate to the correct file format for the target cluster.
|
||||
class ExportFileJob(WriteFileJob):
|
||||
"""Job that exports the build plate to the correct file format for the target cluster."""
|
||||
|
||||
def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str) -> None:
|
||||
|
||||
|
@ -27,12 +27,14 @@ class ExportFileJob(WriteFileJob):
|
|||
extension = self._mesh_format_handler.preferred_format.get("extension", "")
|
||||
self.setFileName("{}.{}".format(job_name, extension))
|
||||
|
||||
## Get the mime type of the selected export file type.
|
||||
def getMimeType(self) -> str:
|
||||
"""Get the mime type of the selected export file type."""
|
||||
|
||||
return self._mesh_format_handler.mime_type
|
||||
|
||||
## Get the job result as bytes as that is what we need to upload to the cluster.
|
||||
def getOutput(self) -> bytes:
|
||||
"""Get the job result as bytes as that is what we need to upload to the cluster."""
|
||||
|
||||
output = self.getStream().getvalue()
|
||||
if isinstance(output, str):
|
||||
output = output.encode("utf-8")
|
||||
|
|
|
@ -16,8 +16,9 @@ from cura.CuraApplication import CuraApplication
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## This class is responsible for choosing the formats used by the connected clusters.
|
||||
class MeshFormatHandler:
|
||||
"""This class is responsible for choosing the formats used by the connected clusters."""
|
||||
|
||||
|
||||
def __init__(self, file_handler: Optional[FileHandler], firmware_version: str) -> None:
|
||||
self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler()
|
||||
|
@ -28,42 +29,50 @@ class MeshFormatHandler:
|
|||
def is_valid(self) -> bool:
|
||||
return bool(self._writer)
|
||||
|
||||
## Chooses the preferred file format.
|
||||
# \return A dict with the file format details, with the following keys:
|
||||
# {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool}
|
||||
@property
|
||||
def preferred_format(self) -> Dict[str, Union[str, int, bool]]:
|
||||
"""Chooses the preferred file format.
|
||||
|
||||
:return: A dict with the file format details, with the following keys:
|
||||
{id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool}
|
||||
"""
|
||||
return self._preferred_format
|
||||
|
||||
## Gets the file writer for the given file handler and mime type.
|
||||
# \return A file writer.
|
||||
@property
|
||||
def writer(self) -> Optional[FileWriter]:
|
||||
"""Gets the file writer for the given file handler and mime type.
|
||||
|
||||
:return: A file writer.
|
||||
"""
|
||||
return self._writer
|
||||
|
||||
@property
|
||||
def mime_type(self) -> str:
|
||||
return cast(str, self._preferred_format["mime_type"])
|
||||
|
||||
## Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)
|
||||
@property
|
||||
def file_mode(self) -> int:
|
||||
"""Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)"""
|
||||
|
||||
return cast(int, self._preferred_format["mode"])
|
||||
|
||||
## Gets the file extension
|
||||
@property
|
||||
def file_extension(self) -> str:
|
||||
"""Gets the file extension"""
|
||||
|
||||
return cast(str, self._preferred_format["extension"])
|
||||
|
||||
## Creates the right kind of stream based on the preferred format.
|
||||
def createStream(self) -> Union[io.BytesIO, io.StringIO]:
|
||||
"""Creates the right kind of stream based on the preferred format."""
|
||||
|
||||
if self.file_mode == FileWriter.OutputMode.TextMode:
|
||||
return io.StringIO()
|
||||
else:
|
||||
return io.BytesIO()
|
||||
|
||||
## Writes the mesh and returns its value.
|
||||
def getBytes(self, nodes: List[SceneNode]) -> bytes:
|
||||
"""Writes the mesh and returns its value."""
|
||||
|
||||
if self.writer is None:
|
||||
raise ValueError("There is no writer for the mesh format handler.")
|
||||
stream = self.createStream()
|
||||
|
@ -73,10 +82,12 @@ class MeshFormatHandler:
|
|||
value = value.encode()
|
||||
return value
|
||||
|
||||
## Chooses the preferred file format for the given file handler.
|
||||
# \param firmware_version: The version of the firmware.
|
||||
# \return A dict with the file format details.
|
||||
def _getPreferredFormat(self, firmware_version: str) -> Dict[str, Union[str, int, bool]]:
|
||||
"""Chooses the preferred file format for the given file handler.
|
||||
|
||||
:param firmware_version: The version of the firmware.
|
||||
:return: A dict with the file format details.
|
||||
"""
|
||||
# Formats supported by this application (file types that we can actually write).
|
||||
application = CuraApplication.getInstance()
|
||||
|
||||
|
@ -108,9 +119,11 @@ class MeshFormatHandler:
|
|||
)
|
||||
return file_formats[0]
|
||||
|
||||
## Gets the file writer for the given file handler and mime type.
|
||||
# \param mime_type: The mine type.
|
||||
# \return A file writer.
|
||||
def _getWriter(self, mime_type: str) -> Optional[FileWriter]:
|
||||
"""Gets the file writer for the given file handler and mime type.
|
||||
|
||||
:param mime_type: The mine type.
|
||||
:return: A file writer.
|
||||
"""
|
||||
# Just take the first file format available.
|
||||
return self._file_handler.getWriterByMimeType(mime_type)
|
||||
|
|
|
@ -7,12 +7,12 @@ from UM.Message import Message
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Message shown when trying to connect to a legacy printer device.
|
||||
class LegacyDeviceNoLongerSupportedMessage(Message):
|
||||
|
||||
# Singleton used to prevent duplicate messages of this type at the same time.
|
||||
"""Message shown when trying to connect to a legacy printer device."""
|
||||
|
||||
__is_visible = False
|
||||
|
||||
"""Singleton used to prevent duplicate messages of this type at the same time."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not "
|
||||
|
|
|
@ -13,11 +13,11 @@ if TYPE_CHECKING:
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Message shown when sending material files to cluster host.
|
||||
class MaterialSyncMessage(Message):
|
||||
"""Message shown when sending material files to cluster host."""
|
||||
|
||||
# Singleton used to prevent duplicate messages of this type at the same time.
|
||||
__is_visible = False
|
||||
"""Singleton used to prevent duplicate messages of this type at the same time."""
|
||||
|
||||
def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None:
|
||||
super().__init__(
|
||||
|
|
|
@ -16,11 +16,11 @@ if TYPE_CHECKING:
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Message shown when trying to connect to a printer that is not a host.
|
||||
class NotClusterHostMessage(Message):
|
||||
"""Message shown when trying to connect to a printer that is not a host."""
|
||||
|
||||
# Singleton used to prevent duplicate messages of this type at the same time.
|
||||
__is_visible = False
|
||||
"""Singleton used to prevent duplicate messages of this type at the same time."""
|
||||
|
||||
def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None:
|
||||
super().__init__(
|
||||
|
|
|
@ -7,9 +7,9 @@ from UM.Message import Message
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Message shown when uploading a print job to a cluster is blocked because another upload is already in progress.
|
||||
class PrintJobUploadBlockedMessage(Message):
|
||||
|
||||
"""Message shown when uploading a print job to a cluster is blocked because another upload is already in progress."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."),
|
||||
|
|
|
@ -7,9 +7,9 @@ from UM.Message import Message
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Message shown when uploading a print job to a cluster failed.
|
||||
class PrintJobUploadErrorMessage(Message):
|
||||
|
||||
"""Message shown when uploading a print job to a cluster failed."""
|
||||
|
||||
def __init__(self, message: str = None) -> None:
|
||||
super().__init__(
|
||||
text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),
|
||||
|
|
|
@ -7,8 +7,9 @@ from UM.Message import Message
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Class responsible for showing a progress message while a mesh is being uploaded to the cloud.
|
||||
class PrintJobUploadProgressMessage(Message):
|
||||
"""Class responsible for showing a progress message while a mesh is being uploaded to the cloud."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"),
|
||||
|
@ -19,14 +20,17 @@ class PrintJobUploadProgressMessage(Message):
|
|||
use_inactivity_timer = False
|
||||
)
|
||||
|
||||
## Shows the progress message.
|
||||
def show(self):
|
||||
"""Shows the progress message."""
|
||||
|
||||
self.setProgress(0)
|
||||
super().show()
|
||||
|
||||
## Updates the percentage of the uploaded.
|
||||
# \param percentage: The percentage amount (0-100).
|
||||
def update(self, percentage: int) -> None:
|
||||
"""Updates the percentage of the uploaded.
|
||||
|
||||
:param percentage: The percentage amount (0-100).
|
||||
"""
|
||||
if not self._visible:
|
||||
super().show()
|
||||
self.setProgress(percentage)
|
||||
|
|
|
@ -7,9 +7,9 @@ from UM.Message import Message
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Message shown when uploading a print job to a cluster succeeded.
|
||||
class PrintJobUploadSuccessMessage(Message):
|
||||
|
||||
"""Message shown when uploading a print job to a cluster succeeded."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
|
||||
|
|
|
@ -18,45 +18,56 @@ class BaseModel:
|
|||
def validate(self) -> None:
|
||||
pass
|
||||
|
||||
## Checks whether the two models are equal.
|
||||
# \param other: The other model.
|
||||
# \return True if they are equal, False if they are different.
|
||||
def __eq__(self, other):
|
||||
"""Checks whether the two models are equal.
|
||||
|
||||
:param other: The other model.
|
||||
:return: True if they are equal, False if they are different.
|
||||
"""
|
||||
return type(self) == type(other) and self.toDict() == other.toDict()
|
||||
|
||||
## Checks whether the two models are different.
|
||||
# \param other: The other model.
|
||||
# \return True if they are different, False if they are the same.
|
||||
def __ne__(self, other) -> bool:
|
||||
"""Checks whether the two models are different.
|
||||
|
||||
:param other: The other model.
|
||||
:return: True if they are different, False if they are the same.
|
||||
"""
|
||||
return type(self) != type(other) or self.toDict() != other.toDict()
|
||||
|
||||
## Converts the model into a serializable dictionary
|
||||
def toDict(self) -> Dict[str, Any]:
|
||||
"""Converts the model into a serializable dictionary"""
|
||||
|
||||
return self.__dict__
|
||||
|
||||
## Parses a single model.
|
||||
# \param model_class: The model class.
|
||||
# \param values: The value of the model, which is usually a dictionary, but may also be already parsed.
|
||||
# \return An instance of the model_class given.
|
||||
@staticmethod
|
||||
def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T:
|
||||
"""Parses a single model.
|
||||
|
||||
:param model_class: The model class.
|
||||
:param values: The value of the model, which is usually a dictionary, but may also be already parsed.
|
||||
:return: An instance of the model_class given.
|
||||
"""
|
||||
if isinstance(values, dict):
|
||||
return model_class(**values)
|
||||
return values
|
||||
|
||||
## Parses a list of models.
|
||||
# \param model_class: The model class.
|
||||
# \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed.
|
||||
# \return A list of instances of the model_class given.
|
||||
@classmethod
|
||||
def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]:
|
||||
"""Parses a list of models.
|
||||
|
||||
:param model_class: The model class.
|
||||
:param values: The value of the list. Each value is usually a dictionary, but may also be already parsed.
|
||||
:return: A list of instances of the model_class given.
|
||||
"""
|
||||
return [cls.parseModel(model_class, value) for value in values]
|
||||
|
||||
## Parses the given date string.
|
||||
# \param date: The date to parse.
|
||||
# \return The parsed date.
|
||||
@staticmethod
|
||||
def parseDate(date: Union[str, datetime]) -> datetime:
|
||||
"""Parses the given date string.
|
||||
|
||||
:param date: The date to parse.
|
||||
:return: The parsed date.
|
||||
"""
|
||||
if isinstance(date, datetime):
|
||||
return date
|
||||
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
|
||||
|
|
|
@ -5,22 +5,26 @@ from typing import Optional
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing a cloud connected cluster.
|
||||
class CloudClusterResponse(BaseModel):
|
||||
"""Class representing a cloud connected cluster."""
|
||||
|
||||
|
||||
## Creates a new cluster response object.
|
||||
# \param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='.
|
||||
# \param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'.
|
||||
# \param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users.
|
||||
# \param is_online: Whether this cluster is currently connected to the cloud.
|
||||
# \param status: The status of the cluster authentication (active or inactive).
|
||||
# \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
|
||||
# \param host_internal_ip: The internal IP address of the host printer.
|
||||
# \param friendly_name: The human readable name of the host printer.
|
||||
# \param printer_type: The machine type of the host printer.
|
||||
def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
|
||||
host_internal_ip: Optional[str] = None, host_version: Optional[str] = None,
|
||||
friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", **kwargs) -> None:
|
||||
"""Creates a new cluster response object.
|
||||
|
||||
:param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='.
|
||||
:param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'.
|
||||
:param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users.
|
||||
:param is_online: Whether this cluster is currently connected to the cloud.
|
||||
:param status: The status of the cluster authentication (active or inactive).
|
||||
:param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
|
||||
:param host_internal_ip: The internal IP address of the host printer.
|
||||
:param friendly_name: The human readable name of the host printer.
|
||||
:param printer_type: The machine type of the host printer.
|
||||
"""
|
||||
|
||||
self.cluster_id = cluster_id
|
||||
self.host_guid = host_guid
|
||||
self.host_name = host_name
|
||||
|
|
|
@ -11,15 +11,17 @@ from .ClusterPrintJobStatus import ClusterPrintJobStatus
|
|||
# Model that represents the status of the cluster for the cloud
|
||||
class CloudClusterStatus(BaseModel):
|
||||
|
||||
## Creates a new cluster status model object.
|
||||
# \param printers: The latest status of each printer in the cluster.
|
||||
# \param print_jobs: The latest status of each print job in the cluster.
|
||||
# \param generated_time: The datetime when the object was generated on the server-side.
|
||||
def __init__(self,
|
||||
printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]],
|
||||
def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]],
|
||||
print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]],
|
||||
generated_time: Union[str, datetime],
|
||||
**kwargs) -> None:
|
||||
"""Creates a new cluster status model object.
|
||||
|
||||
:param printers: The latest status of each printer in the cluster.
|
||||
:param print_jobs: The latest status of each print job in the cluster.
|
||||
:param generated_time: The datetime when the object was generated on the server-side.
|
||||
"""
|
||||
|
||||
self.generated_time = self.parseDate(generated_time)
|
||||
self.printers = self.parseModels(ClusterPrinterStatus, printers)
|
||||
self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs)
|
||||
|
|
|
@ -5,20 +5,23 @@ from typing import Dict, Optional, Any
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing errors generated by the cloud servers, according to the JSON-API standard.
|
||||
class CloudError(BaseModel):
|
||||
"""Class representing errors generated by the cloud servers, according to the JSON-API standard."""
|
||||
|
||||
## Creates a new error object.
|
||||
# \param id: Unique identifier for this particular occurrence of the problem.
|
||||
# \param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence
|
||||
# of the problem, except for purposes of localization.
|
||||
# \param code: An application-specific error code, expressed as a string value.
|
||||
# \param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's
|
||||
# value can be localized.
|
||||
# \param http_status: The HTTP status code applicable to this problem, converted to string.
|
||||
# \param meta: Non-standard meta-information about the error, depending on the error code.
|
||||
def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None,
|
||||
meta: Optional[Dict[str, Any]] = None, **kwargs) -> None:
|
||||
"""Creates a new error object.
|
||||
|
||||
:param id: Unique identifier for this particular occurrence of the problem.
|
||||
:param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence
|
||||
of the problem, except for purposes of localization.
|
||||
:param code: An application-specific error code, expressed as a string value.
|
||||
:param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's
|
||||
value can be localized.
|
||||
:param http_status: The HTTP status code applicable to this problem, converted to string.
|
||||
:param meta: Non-standard meta-information about the error, depending on the error code.
|
||||
"""
|
||||
|
||||
self.id = id
|
||||
self.code = code
|
||||
self.http_status = http_status
|
||||
|
|
|
@ -8,19 +8,22 @@ from ..BaseModel import BaseModel
|
|||
# Model that represents the response received from the cloud after requesting to upload a print job
|
||||
class CloudPrintJobResponse(BaseModel):
|
||||
|
||||
## Creates a new print job response model.
|
||||
# \param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='.
|
||||
# \param status: The status of the print job.
|
||||
# \param status_description: Contains more details about the status, e.g. the cause of failures.
|
||||
# \param download_url: A signed URL to download the resulting status. Only available when the job is finished.
|
||||
# \param job_name: The name of the print job.
|
||||
# \param slicing_details: Model for slice information.
|
||||
# \param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading).
|
||||
# \param content_type: The content type of the print job (e.g. text/plain or application/gzip)
|
||||
# \param generated_time: The datetime when the object was generated on the server-side.
|
||||
def __init__(self, job_id: str, status: str, download_url: Optional[str] = None, job_name: Optional[str] = None,
|
||||
upload_url: Optional[str] = None, content_type: Optional[str] = None,
|
||||
status_description: Optional[str] = None, slicing_details: Optional[dict] = None, **kwargs) -> None:
|
||||
"""Creates a new print job response model.
|
||||
|
||||
:param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='.
|
||||
:param status: The status of the print job.
|
||||
:param status_description: Contains more details about the status, e.g. the cause of failures.
|
||||
:param download_url: A signed URL to download the resulting status. Only available when the job is finished.
|
||||
:param job_name: The name of the print job.
|
||||
:param slicing_details: Model for slice information.
|
||||
:param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading).
|
||||
:param content_type: The content type of the print job (e.g. text/plain or application/gzip)
|
||||
:param generated_time: The datetime when the object was generated on the server-side.
|
||||
"""
|
||||
|
||||
self.job_id = job_id
|
||||
self.status = status
|
||||
self.download_url = download_url
|
||||
|
|
|
@ -6,11 +6,14 @@ from ..BaseModel import BaseModel
|
|||
# Model that represents the request to upload a print job to the cloud
|
||||
class CloudPrintJobUploadRequest(BaseModel):
|
||||
|
||||
## Creates a new print job upload request.
|
||||
# \param job_name: The name of the print job.
|
||||
# \param file_size: The size of the file in bytes.
|
||||
# \param content_type: The content type of the print job (e.g. text/plain or application/gzip)
|
||||
def __init__(self, job_name: str, file_size: int, content_type: str, **kwargs) -> None:
|
||||
"""Creates a new print job upload request.
|
||||
|
||||
:param job_name: The name of the print job.
|
||||
:param file_size: The size of the file in bytes.
|
||||
:param content_type: The content type of the print job (e.g. text/plain or application/gzip)
|
||||
"""
|
||||
|
||||
self.job_name = job_name
|
||||
self.file_size = file_size
|
||||
self.content_type = content_type
|
||||
|
|
|
@ -9,13 +9,16 @@ from ..BaseModel import BaseModel
|
|||
# Model that represents the responses received from the cloud after requesting a job to be printed.
|
||||
class CloudPrintResponse(BaseModel):
|
||||
|
||||
## Creates a new print response object.
|
||||
# \param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect.
|
||||
# \param status: The status of the print request (queued or failed).
|
||||
# \param generated_time: The datetime when the object was generated on the server-side.
|
||||
# \param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect.
|
||||
def __init__(self, job_id: str, status: str, generated_time: Union[str, datetime],
|
||||
cluster_job_id: Optional[str] = None, **kwargs) -> None:
|
||||
"""Creates a new print response object.
|
||||
|
||||
:param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect.
|
||||
:param status: The status of the print request (queued or failed).
|
||||
:param generated_time: The datetime when the object was generated on the server-side.
|
||||
:param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect.
|
||||
"""
|
||||
|
||||
self.job_id = job_id
|
||||
self.status = status
|
||||
self.cluster_job_id = cluster_job_id
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing a cluster printer
|
||||
class ClusterBuildPlate(BaseModel):
|
||||
"""Class representing a cluster printer"""
|
||||
|
||||
## Create a new build plate
|
||||
# \param type: The type of build plate glass or aluminium
|
||||
def __init__(self, type: str = "glass", **kwargs) -> None:
|
||||
"""Create a new build plate
|
||||
|
||||
:param type: The type of build plate glass or aluminium
|
||||
"""
|
||||
self.type = type
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -9,26 +9,33 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing a cloud cluster printer configuration
|
||||
# Also used for representing slots in a Material Station (as from Cura's perspective these are the same).
|
||||
class ClusterPrintCoreConfiguration(BaseModel):
|
||||
"""Class representing a cloud cluster printer configuration
|
||||
|
||||
Also used for representing slots in a Material Station (as from Cura's perspective these are the same).
|
||||
"""
|
||||
|
||||
def __init__(self, extruder_index: int, material: Union[None, Dict[str, Any],
|
||||
ClusterPrinterConfigurationMaterial] = None, print_core_id: Optional[str] = None, **kwargs) -> None:
|
||||
"""Creates a new cloud cluster printer configuration object
|
||||
|
||||
:param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right.
|
||||
:param material: The material of a configuration object in a cluster printer. May be in a dict or an object.
|
||||
:param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'.
|
||||
:param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'.
|
||||
"""
|
||||
|
||||
## Creates a new cloud cluster printer configuration object
|
||||
# \param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right.
|
||||
# \param material: The material of a configuration object in a cluster printer. May be in a dict or an object.
|
||||
# \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'.
|
||||
# \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'.
|
||||
def __init__(self, extruder_index: int,
|
||||
material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None,
|
||||
print_core_id: Optional[str] = None, **kwargs) -> None:
|
||||
self.extruder_index = extruder_index
|
||||
self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None
|
||||
self.print_core_id = print_core_id
|
||||
super().__init__(**kwargs)
|
||||
|
||||
## Updates the given output model.
|
||||
# \param model - The output model to update.
|
||||
def updateOutputModel(self, model: ExtruderOutputModel) -> None:
|
||||
"""Updates the given output model.
|
||||
|
||||
:param model: The output model to update.
|
||||
"""
|
||||
|
||||
if self.print_core_id is not None:
|
||||
model.updateHotendID(self.print_core_id)
|
||||
|
||||
|
@ -40,14 +47,16 @@ class ClusterPrintCoreConfiguration(BaseModel):
|
|||
else:
|
||||
model.updateActiveMaterial(None)
|
||||
|
||||
## Creates a configuration model
|
||||
def createConfigurationModel(self) -> ExtruderConfigurationModel:
|
||||
"""Creates a configuration model"""
|
||||
|
||||
model = ExtruderConfigurationModel(position = self.extruder_index)
|
||||
self.updateConfigurationModel(model)
|
||||
return model
|
||||
|
||||
## Creates a configuration model
|
||||
def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel:
|
||||
"""Creates a configuration model"""
|
||||
|
||||
model.setHotendID(self.print_core_id)
|
||||
if self.material:
|
||||
model.setMaterial(self.material.createOutputModel())
|
||||
|
|
|
@ -5,19 +5,22 @@ from typing import Optional
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Model for the types of changes that are needed before a print job can start
|
||||
class ClusterPrintJobConfigurationChange(BaseModel):
|
||||
"""Model for the types of changes that are needed before a print job can start"""
|
||||
|
||||
|
||||
def __init__(self, type_of_change: str, target_id: str, origin_id: str, index: Optional[int] = None,
|
||||
target_name: Optional[str] = None, origin_name: Optional[str] = None, **kwargs) -> None:
|
||||
"""Creates a new print job constraint.
|
||||
|
||||
:param type_of_change: The type of configuration change, one of: "material", "print_core_change"
|
||||
:param index: The hotend slot or extruder index to change
|
||||
:param target_id: Target material guid or hotend id
|
||||
:param origin_id: Original/current material guid or hotend id
|
||||
:param target_name: Target material name or hotend id
|
||||
:param origin_name: Original/current material name or hotend id
|
||||
"""
|
||||
|
||||
## Creates a new print job constraint.
|
||||
# \param type_of_change: The type of configuration change, one of: "material", "print_core_change"
|
||||
# \param index: The hotend slot or extruder index to change
|
||||
# \param target_id: Target material guid or hotend id
|
||||
# \param origin_id: Original/current material guid or hotend id
|
||||
# \param target_name: Target material name or hotend id
|
||||
# \param origin_name: Original/current material name or hotend id
|
||||
def __init__(self, type_of_change: str, target_id: str, origin_id: str,
|
||||
index: Optional[int] = None, target_name: Optional[str] = None, origin_name: Optional[str] = None,
|
||||
**kwargs) -> None:
|
||||
self.type_of_change = type_of_change
|
||||
self.index = index
|
||||
self.target_id = target_id
|
||||
|
|
|
@ -5,12 +5,14 @@ from typing import Optional
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing a cloud cluster print job constraint
|
||||
class ClusterPrintJobConstraints(BaseModel):
|
||||
"""Class representing a cloud cluster print job constraint"""
|
||||
|
||||
## Creates a new print job constraint.
|
||||
# \param require_printer_name: Unique name of the printer that this job should be printed on.
|
||||
# Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec'
|
||||
def __init__(self, require_printer_name: Optional[str] = None, **kwargs) -> None:
|
||||
"""Creates a new print job constraint.
|
||||
|
||||
:param require_printer_name: Unique name of the printer that this job should be printed on.
|
||||
Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec'
|
||||
"""
|
||||
self.require_printer_name = require_printer_name
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -3,14 +3,17 @@
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing the reasons that prevent this job from being printed on the associated printer
|
||||
class ClusterPrintJobImpediment(BaseModel):
|
||||
"""Class representing the reasons that prevent this job from being printed on the associated printer"""
|
||||
|
||||
## Creates a new print job constraint.
|
||||
# \param translation_key: A string indicating a reason the print cannot be printed,
|
||||
# such as 'does_not_fit_in_build_volume'
|
||||
# \param severity: A number indicating the severity of the problem, with higher being more severe
|
||||
def __init__(self, translation_key: str, severity: int, **kwargs) -> None:
|
||||
"""Creates a new print job constraint.
|
||||
|
||||
:param translation_key: A string indicating a reason the print cannot be printed,
|
||||
such as 'does_not_fit_in_build_volume'
|
||||
:param severity: A number indicating the severity of the problem, with higher being more severe
|
||||
"""
|
||||
|
||||
self.translation_key = translation_key
|
||||
self.severity = severity
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -15,36 +15,9 @@ from ..BaseModel import BaseModel
|
|||
from ...ClusterOutputController import ClusterOutputController
|
||||
|
||||
|
||||
## Model for the status of a single print job in a cluster.
|
||||
class ClusterPrintJobStatus(BaseModel):
|
||||
"""Model for the status of a single print job in a cluster."""
|
||||
|
||||
## Creates a new cloud print job status model.
|
||||
# \param assigned_to: The name of the printer this job is assigned to while being queued.
|
||||
# \param configuration: The required print core configurations of this print job.
|
||||
# \param constraints: Print job constraints object.
|
||||
# \param created_at: The timestamp when the job was created in Cura Connect.
|
||||
# \param force: Allow this job to be printed despite of mismatching configurations.
|
||||
# \param last_seen: The number of seconds since this job was checked.
|
||||
# \param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field
|
||||
# of the printer object.
|
||||
# \param name: The name of the print job. Usually the name of the .gcode file.
|
||||
# \param network_error_count: The number of errors encountered when requesting data for this print job.
|
||||
# \param owner: The name of the user who added the print job to Cura Connect.
|
||||
# \param printer_uuid: UUID of the printer that the job is currently printing on or assigned to.
|
||||
# \param started: Whether the job has started printing or not.
|
||||
# \param status: The status of the print job.
|
||||
# \param time_elapsed: The remaining printing time in seconds.
|
||||
# \param time_total: The total printing time in seconds.
|
||||
# \param uuid: UUID of this print job. Should be used for identification purposes.
|
||||
# \param deleted_at: The time when this print job was deleted.
|
||||
# \param printed_on_uuid: UUID of the printer used to print this job.
|
||||
# \param configuration_changes_required: List of configuration changes the printer this job is associated with
|
||||
# needs to make in order to be able to print this job
|
||||
# \param build_plate: The build plate (type) this job needs to be printed on.
|
||||
# \param compatible_machine_families: Family names of machines suitable for this print job
|
||||
# \param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated
|
||||
# printer
|
||||
# \param preview_url: URL to the preview image (same as wou;d've been included in the ufp).
|
||||
def __init__(self, created_at: str, force: bool, machine_variant: str, name: str, started: bool, status: str,
|
||||
time_total: int, uuid: str,
|
||||
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
|
||||
|
@ -60,6 +33,37 @@ class ClusterPrintJobStatus(BaseModel):
|
|||
impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None,
|
||||
preview_url: Optional[str] = None,
|
||||
**kwargs) -> None:
|
||||
|
||||
"""Creates a new cloud print job status model.
|
||||
|
||||
:param assigned_to: The name of the printer this job is assigned to while being queued.
|
||||
:param configuration: The required print core configurations of this print job.
|
||||
:param constraints: Print job constraints object.
|
||||
:param created_at: The timestamp when the job was created in Cura Connect.
|
||||
:param force: Allow this job to be printed despite of mismatching configurations.
|
||||
:param last_seen: The number of seconds since this job was checked.
|
||||
:param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field
|
||||
of the printer object.
|
||||
:param name: The name of the print job. Usually the name of the .gcode file.
|
||||
:param network_error_count: The number of errors encountered when requesting data for this print job.
|
||||
:param owner: The name of the user who added the print job to Cura Connect.
|
||||
:param printer_uuid: UUID of the printer that the job is currently printing on or assigned to.
|
||||
:param started: Whether the job has started printing or not.
|
||||
:param status: The status of the print job.
|
||||
:param time_elapsed: The remaining printing time in seconds.
|
||||
:param time_total: The total printing time in seconds.
|
||||
:param uuid: UUID of this print job. Should be used for identification purposes.
|
||||
:param deleted_at: The time when this print job was deleted.
|
||||
:param printed_on_uuid: UUID of the printer used to print this job.
|
||||
:param configuration_changes_required: List of configuration changes the printer this job is associated with
|
||||
needs to make in order to be able to print this job
|
||||
:param build_plate: The build plate (type) this job needs to be printed on.
|
||||
:param compatible_machine_families: Family names of machines suitable for this print job
|
||||
:param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated
|
||||
printer
|
||||
:param preview_url: URL to the preview image (same as wou;d've been included in the ufp).
|
||||
"""
|
||||
|
||||
self.assigned_to = assigned_to
|
||||
self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
|
||||
self.constraints = self.parseModels(ClusterPrintJobConstraints, constraints)
|
||||
|
@ -90,24 +94,31 @@ class ClusterPrintJobStatus(BaseModel):
|
|||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
## Creates an UM3 print job output model based on this cloud cluster print job.
|
||||
# \param printer: The output model of the printer
|
||||
def createOutputModel(self, controller: ClusterOutputController) -> UM3PrintJobOutputModel:
|
||||
"""Creates an UM3 print job output model based on this cloud cluster print job.
|
||||
|
||||
:param printer: The output model of the printer
|
||||
"""
|
||||
|
||||
model = UM3PrintJobOutputModel(controller, self.uuid, self.name)
|
||||
self.updateOutputModel(model)
|
||||
return model
|
||||
|
||||
## Creates a new configuration model
|
||||
def _createConfigurationModel(self) -> PrinterConfigurationModel:
|
||||
"""Creates a new configuration model"""
|
||||
|
||||
extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
|
||||
configuration = PrinterConfigurationModel()
|
||||
configuration.setExtruderConfigurations(extruders)
|
||||
configuration.setPrinterType(self.machine_variant)
|
||||
return configuration
|
||||
|
||||
## Updates an UM3 print job output model based on this cloud cluster print job.
|
||||
# \param model: The model to update.
|
||||
def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None:
|
||||
"""Updates an UM3 print job output model based on this cloud cluster print job.
|
||||
|
||||
:param model: The model to update.
|
||||
"""
|
||||
|
||||
model.updateConfiguration(self._createConfigurationModel())
|
||||
model.updateTimeTotal(self.time_total)
|
||||
model.updateTimeElapsed(self.time_elapsed)
|
||||
|
|
|
@ -9,29 +9,35 @@ from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing a cloud cluster printer configuration
|
||||
class ClusterPrinterConfigurationMaterial(BaseModel):
|
||||
"""Class representing a cloud cluster printer configuration"""
|
||||
|
||||
## Creates a new material configuration model.
|
||||
# \param brand: The brand of material in this print core, e.g. 'Ultimaker'.
|
||||
# \param color: The color of material in this print core, e.g. 'Blue'.
|
||||
# \param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'.
|
||||
# \param material: The type of material in this print core, e.g. 'PLA'.
|
||||
def __init__(self, brand: Optional[str] = None, color: Optional[str] = None, guid: Optional[str] = None,
|
||||
material: Optional[str] = None, **kwargs) -> None:
|
||||
|
||||
"""Creates a new material configuration model.
|
||||
|
||||
:param brand: The brand of material in this print core, e.g. 'Ultimaker'.
|
||||
:param color: The color of material in this print core, e.g. 'Blue'.
|
||||
:param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'.
|
||||
:param material: The type of material in this print core, e.g. 'PLA'.
|
||||
"""
|
||||
|
||||
self.guid = guid
|
||||
self.brand = brand
|
||||
self.color = color
|
||||
self.material = material
|
||||
super().__init__(**kwargs)
|
||||
|
||||
## Creates a material output model based on this cloud printer material.
|
||||
#
|
||||
# A material is chosen that matches the current GUID. If multiple such
|
||||
# materials are available, read-only materials are preferred and the
|
||||
# material with the earliest alphabetical name will be selected.
|
||||
# \return A material output model that matches the current GUID.
|
||||
def createOutputModel(self) -> MaterialOutputModel:
|
||||
"""Creates a material output model based on this cloud printer material.
|
||||
|
||||
A material is chosen that matches the current GUID. If multiple such
|
||||
materials are available, read-only materials are preferred and the
|
||||
material with the earliest alphabetical name will be selected.
|
||||
:return: A material output model that matches the current GUID.
|
||||
"""
|
||||
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid)
|
||||
if same_guid:
|
||||
|
|
|
@ -6,16 +6,19 @@ from ..BaseModel import BaseModel
|
|||
from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot
|
||||
|
||||
|
||||
## Class representing the data of a Material Station in the cluster.
|
||||
class ClusterPrinterMaterialStation(BaseModel):
|
||||
"""Class representing the data of a Material Station in the cluster."""
|
||||
|
||||
## Creates a new Material Station status.
|
||||
# \param status: The status of the material station.
|
||||
# \param: supported: Whether the material station is supported on this machine or not.
|
||||
# \param material_slots: The active slots configurations of this material station.
|
||||
def __init__(self, status: str, supported: bool = False,
|
||||
material_slots: List[Union[ClusterPrinterMaterialStationSlot, Dict[str, Any]]] = None,
|
||||
**kwargs) -> None:
|
||||
"""Creates a new Material Station status.
|
||||
|
||||
:param status: The status of the material station.
|
||||
:param: supported: Whether the material station is supported on this machine or not.
|
||||
:param material_slots: The active slots configurations of this material station.
|
||||
"""
|
||||
|
||||
self.status = status
|
||||
self.supported = supported
|
||||
self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\
|
||||
|
|
|
@ -5,16 +5,19 @@ from typing import Optional
|
|||
from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
|
||||
|
||||
|
||||
## Class representing the data of a single slot in the material station.
|
||||
class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration):
|
||||
|
||||
## Create a new material station slot object.
|
||||
# \param slot_index: The index of the slot in the material station (ranging 0 to 5).
|
||||
# \param compatible: Whether the configuration is compatible with the print core.
|
||||
# \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data).
|
||||
# \param material_empty: Whether the material spool is too empty to be used.
|
||||
"""Class representing the data of a single slot in the material station."""
|
||||
|
||||
def __init__(self, slot_index: int, compatible: bool, material_remaining: float,
|
||||
material_empty: Optional[bool] = False, **kwargs) -> None:
|
||||
"""Create a new material station slot object.
|
||||
|
||||
:param slot_index: The index of the slot in the material station (ranging 0 to 5).
|
||||
:param compatible: Whether the configuration is compatible with the print core.
|
||||
:param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data).
|
||||
:param material_empty: Whether the material spool is too empty to be used.
|
||||
"""
|
||||
|
||||
self.slot_index = slot_index
|
||||
self.compatible = compatible
|
||||
self.material_remaining = material_remaining
|
||||
|
|
|
@ -17,26 +17,10 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing a cluster printer
|
||||
class ClusterPrinterStatus(BaseModel):
|
||||
"""Class representing a cluster printer"""
|
||||
|
||||
|
||||
## Creates a new cluster printer status
|
||||
# \param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled.
|
||||
# \param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster.
|
||||
# \param friendly_name: Human readable name of the printer. Can be used for identification purposes.
|
||||
# \param ip_address: The IP address of the printer in the local network.
|
||||
# \param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'.
|
||||
# \param status: The status of the printer.
|
||||
# \param unique_name: The unique name of the printer in the network.
|
||||
# \param uuid: The unique ID of the printer, also known as GUID.
|
||||
# \param configuration: The active print core configurations of this printer.
|
||||
# \param reserved_by: A printer can be claimed by a specific print job.
|
||||
# \param maintenance_required: Indicates if maintenance is necessary.
|
||||
# \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date",
|
||||
# "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible".
|
||||
# \param latest_available_firmware: The version of the latest firmware that is available.
|
||||
# \param build_plate: The build plate that is on the printer.
|
||||
# \param material_station: The material station that is on the printer.
|
||||
def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
|
||||
status: str, unique_name: str, uuid: str,
|
||||
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
|
||||
|
@ -44,6 +28,25 @@ class ClusterPrinterStatus(BaseModel):
|
|||
firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None,
|
||||
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None,
|
||||
material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None:
|
||||
"""Creates a new cluster printer status
|
||||
|
||||
:param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled.
|
||||
:param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster.
|
||||
:param friendly_name: Human readable name of the printer. Can be used for identification purposes.
|
||||
:param ip_address: The IP address of the printer in the local network.
|
||||
:param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'.
|
||||
:param status: The status of the printer.
|
||||
:param unique_name: The unique name of the printer in the network.
|
||||
:param uuid: The unique ID of the printer, also known as GUID.
|
||||
:param configuration: The active print core configurations of this printer.
|
||||
:param reserved_by: A printer can be claimed by a specific print job.
|
||||
:param maintenance_required: Indicates if maintenance is necessary.
|
||||
:param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date",
|
||||
"pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible".
|
||||
:param latest_available_firmware: The version of the latest firmware that is available.
|
||||
:param build_plate: The build plate that is on the printer.
|
||||
:param material_station: The material station that is on the printer.
|
||||
"""
|
||||
|
||||
self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
|
||||
self.enabled = enabled
|
||||
|
@ -63,9 +66,12 @@ class ClusterPrinterStatus(BaseModel):
|
|||
material_station) if material_station else None
|
||||
super().__init__(**kwargs)
|
||||
|
||||
## Creates a new output model.
|
||||
# \param controller - The controller of the model.
|
||||
def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel:
|
||||
"""Creates a new output model.
|
||||
|
||||
:param controller: - The controller of the model.
|
||||
"""
|
||||
|
||||
# FIXME
|
||||
# Note that we're using '2' here as extruder count. We have hardcoded this for now to prevent issues where the
|
||||
# amount of extruders coming back from the API is actually lower (which it can be if a printer was just added
|
||||
|
@ -74,9 +80,12 @@ class ClusterPrinterStatus(BaseModel):
|
|||
self.updateOutputModel(model)
|
||||
return model
|
||||
|
||||
## Updates the given output model.
|
||||
# \param model - The output model to update.
|
||||
def updateOutputModel(self, model: PrinterOutputModel) -> None:
|
||||
"""Updates the given output model.
|
||||
|
||||
:param model: - The output model to update.
|
||||
"""
|
||||
|
||||
model.updateKey(self.uuid)
|
||||
model.updateName(self.friendly_name)
|
||||
model.updateUniqueName(self.unique_name)
|
||||
|
@ -110,9 +119,12 @@ class ClusterPrinterStatus(BaseModel):
|
|||
) for left_slot, right_slot in product(self._getSlotsForExtruder(0), self._getSlotsForExtruder(1))]
|
||||
model.setAvailableConfigurations(available_configurations)
|
||||
|
||||
## Create a list of Material Station slots for the given extruder index.
|
||||
# Returns a list with a single empty material slot if none are found to ensure we don't miss configurations.
|
||||
def _getSlotsForExtruder(self, extruder_index: int) -> List[ClusterPrinterMaterialStationSlot]:
|
||||
"""Create a list of Material Station slots for the given extruder index.
|
||||
|
||||
Returns a list with a single empty material slot if none are found to ensure we don't miss configurations.
|
||||
"""
|
||||
|
||||
if not self.material_station: # typing guard
|
||||
return []
|
||||
slots = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration(
|
||||
|
@ -121,15 +133,19 @@ class ClusterPrinterStatus(BaseModel):
|
|||
)]
|
||||
return slots or [self._createEmptyMaterialSlot(extruder_index)]
|
||||
|
||||
## Check if a configuration is supported in order to make it selectable by the user.
|
||||
# We filter out any slot that is not supported by the extruder index, print core type or if the material is empty.
|
||||
@staticmethod
|
||||
def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool:
|
||||
"""Check if a configuration is supported in order to make it selectable by the user.
|
||||
|
||||
We filter out any slot that is not supported by the extruder index, print core type or if the material is empty.
|
||||
"""
|
||||
|
||||
return slot.extruder_index == extruder_index and slot.compatible and not slot.material_empty
|
||||
|
||||
## Create an empty material slot with a fake empty material.
|
||||
@staticmethod
|
||||
def _createEmptyMaterialSlot(extruder_index: int) -> ClusterPrinterMaterialStationSlot:
|
||||
"""Create an empty material slot with a fake empty material."""
|
||||
|
||||
empty_material = ClusterPrinterConfigurationMaterial(guid = "", material = "empty", brand = "", color = "")
|
||||
return ClusterPrinterMaterialStationSlot(slot_index = 0, extruder_index = extruder_index,
|
||||
compatible = True, material_remaining = 0, material = empty_material)
|
||||
|
|
|
@ -5,12 +5,11 @@ from typing import Dict, Any
|
|||
from ..BaseModel import BaseModel
|
||||
|
||||
|
||||
## Class representing the system status of a printer.
|
||||
class PrinterSystemStatus(BaseModel):
|
||||
"""Class representing the system status of a printer."""
|
||||
|
||||
def __init__(self, guid: str, firmware: str, hostname: str, name: str, platform: str, variant: str,
|
||||
hardware: Dict[str, Any], **kwargs
|
||||
) -> None:
|
||||
hardware: Dict[str, Any], **kwargs) -> None:
|
||||
self.guid = guid
|
||||
self.firmware = firmware
|
||||
self.hostname = hostname
|
||||
|
|
|
@ -16,12 +16,13 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus
|
|||
from ..Models.Http.ClusterMaterial import ClusterMaterial
|
||||
|
||||
|
||||
## The generic type variable used to document the methods below.
|
||||
ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel)
|
||||
"""The generic type variable used to document the methods below."""
|
||||
|
||||
|
||||
## The ClusterApiClient is responsible for all network calls to local network clusters.
|
||||
class ClusterApiClient:
|
||||
"""The ClusterApiClient is responsible for all network calls to local network clusters."""
|
||||
|
||||
|
||||
PRINTER_API_PREFIX = "/api/v1"
|
||||
CLUSTER_API_PREFIX = "/cluster-api/v1"
|
||||
|
@ -29,75 +30,92 @@ class ClusterApiClient:
|
|||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
_anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
||||
|
||||
## Initializes a new cluster API client.
|
||||
# \param address: The network address of the cluster to call.
|
||||
# \param on_error: The callback to be called whenever we receive errors from the server.
|
||||
def __init__(self, address: str, on_error: Callable) -> None:
|
||||
"""Initializes a new cluster API client.
|
||||
|
||||
:param address: The network address of the cluster to call.
|
||||
:param on_error: The callback to be called whenever we receive errors from the server.
|
||||
"""
|
||||
super().__init__()
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._address = address
|
||||
self._on_error = on_error
|
||||
|
||||
## Get printer system information.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
def getSystem(self, on_finished: Callable) -> None:
|
||||
"""Get printer system information.
|
||||
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
"""
|
||||
url = "{}/system".format(self.PRINTER_API_PREFIX)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, PrinterSystemStatus)
|
||||
|
||||
## Get the installed materials on the printer.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None:
|
||||
"""Get the installed materials on the printer.
|
||||
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
"""
|
||||
url = "{}/materials".format(self.CLUSTER_API_PREFIX)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, ClusterMaterial)
|
||||
|
||||
## Get the printers in the cluster.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None:
|
||||
"""Get the printers in the cluster.
|
||||
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
"""
|
||||
url = "{}/printers".format(self.CLUSTER_API_PREFIX)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, ClusterPrinterStatus)
|
||||
|
||||
## Get the print jobs in the cluster.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None:
|
||||
"""Get the print jobs in the cluster.
|
||||
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
"""
|
||||
url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, ClusterPrintJobStatus)
|
||||
|
||||
## Move a print job to the top of the queue.
|
||||
def movePrintJobToTop(self, print_job_uuid: str) -> None:
|
||||
"""Move a print job to the top of the queue."""
|
||||
|
||||
url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid)
|
||||
self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode())
|
||||
|
||||
## Override print job configuration and force it to be printed.
|
||||
def forcePrintJob(self, print_job_uuid: str) -> None:
|
||||
"""Override print job configuration and force it to be printed."""
|
||||
|
||||
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
|
||||
self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode())
|
||||
|
||||
## Delete a print job from the queue.
|
||||
def deletePrintJob(self, print_job_uuid: str) -> None:
|
||||
"""Delete a print job from the queue."""
|
||||
|
||||
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
|
||||
self._manager.deleteResource(self._createEmptyRequest(url))
|
||||
|
||||
## Set the state of a print job.
|
||||
def setPrintJobState(self, print_job_uuid: str, state: str) -> None:
|
||||
"""Set the state of a print job."""
|
||||
|
||||
url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid)
|
||||
# We rewrite 'resume' to 'print' here because we are using the old print job action endpoints.
|
||||
action = "print" if state == "resume" else state
|
||||
self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode())
|
||||
|
||||
## Get the preview image data of a print job.
|
||||
def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None:
|
||||
"""Get the preview image data of a print job."""
|
||||
|
||||
url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished)
|
||||
|
||||
## We override _createEmptyRequest in order to add the user credentials.
|
||||
# \param url: The URL to request
|
||||
# \param content_type: The type of the body contents.
|
||||
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
|
||||
"""We override _createEmptyRequest in order to add the user credentials.
|
||||
|
||||
:param url: The URL to request
|
||||
:param content_type: The type of the body contents.
|
||||
"""
|
||||
url = QUrl("http://" + self._address + path)
|
||||
request = QNetworkRequest(url)
|
||||
request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
|
||||
|
@ -105,11 +123,13 @@ class ClusterApiClient:
|
|||
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||
return request
|
||||
|
||||
## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
|
||||
# \param reply: The reply from the server.
|
||||
# \return A tuple with a status code and a dictionary.
|
||||
@staticmethod
|
||||
def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]:
|
||||
"""Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
|
||||
|
||||
:param reply: The reply from the server.
|
||||
:return: A tuple with a status code and a dictionary.
|
||||
"""
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
try:
|
||||
response = bytes(reply.readAll()).decode()
|
||||
|
@ -118,14 +138,15 @@ class ClusterApiClient:
|
|||
Logger.logException("e", "Could not parse the cluster response: %s", err)
|
||||
return status_code, {"errors": [err]}
|
||||
|
||||
## Parses the given models and calls the correct callback depending on the result.
|
||||
# \param response: The response from the server, after being converted to a dict.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
# \param model_class: The type of the model to convert the response to. It may either be a single record or a list.
|
||||
def _parseModels(self, response: Dict[str, Any],
|
||||
on_finished: Union[Callable[[ClusterApiClientModel], Any],
|
||||
Callable[[List[ClusterApiClientModel]], Any]],
|
||||
model_class: Type[ClusterApiClientModel]) -> None:
|
||||
def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any],
|
||||
Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None:
|
||||
"""Parses the given models and calls the correct callback depending on the result.
|
||||
|
||||
:param response: The response from the server, after being converted to a dict.
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
:param model_class: The type of the model to convert the response to. It may either be a single record or a list.
|
||||
"""
|
||||
|
||||
try:
|
||||
if isinstance(response, list):
|
||||
results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel]
|
||||
|
@ -138,16 +159,15 @@ class ClusterApiClient:
|
|||
except (JSONDecodeError, TypeError, ValueError):
|
||||
Logger.log("e", "Could not parse response from network: %s", str(response))
|
||||
|
||||
## Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
# The callback is added to the 'finished' signal of the reply.
|
||||
# \param reply: The reply that should be listened to.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
def _addCallback(self,
|
||||
reply: QNetworkReply,
|
||||
on_finished: Union[Callable[[ClusterApiClientModel], Any],
|
||||
Callable[[List[ClusterApiClientModel]], Any]],
|
||||
model: Type[ClusterApiClientModel] = None,
|
||||
def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any],
|
||||
Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None,
|
||||
) -> None:
|
||||
"""Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
|
||||
The callback is added to the 'finished' signal of the reply.
|
||||
:param reply: The reply that should be listened to.
|
||||
:param on_finished: The callback in case the response is successful.
|
||||
"""
|
||||
|
||||
def parse() -> None:
|
||||
self._anti_gc_callbacks.remove(parse)
|
||||
|
|
|
@ -51,15 +51,17 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._setInterfaceElements()
|
||||
self._active_camera_url = QUrl() # type: QUrl
|
||||
|
||||
## Set all the interface elements and texts for this output device.
|
||||
def _setInterfaceElements(self) -> None:
|
||||
"""Set all the interface elements and texts for this output device."""
|
||||
|
||||
self.setPriority(3) # Make sure the output device gets selected above local file output
|
||||
self.setShortDescription(I18N_CATALOG.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
|
||||
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network"))
|
||||
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network"))
|
||||
|
||||
## Called when the connection to the cluster changes.
|
||||
def connect(self) -> None:
|
||||
"""Called when the connection to the cluster changes."""
|
||||
|
||||
super().connect()
|
||||
self._update()
|
||||
self.sendMaterialProfiles()
|
||||
|
@ -94,10 +96,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
def forceSendJob(self, print_job_uuid: str) -> None:
|
||||
self._getApiClient().forcePrintJob(print_job_uuid)
|
||||
|
||||
## Set the remote print job state.
|
||||
# \param print_job_uuid: The UUID of the print job to set the state for.
|
||||
# \param action: The action to undertake ('pause', 'resume', 'abort').
|
||||
def setJobState(self, print_job_uuid: str, action: str) -> None:
|
||||
"""Set the remote print job state.
|
||||
|
||||
:param print_job_uuid: The UUID of the print job to set the state for.
|
||||
:param action: The action to undertake ('pause', 'resume', 'abort').
|
||||
"""
|
||||
|
||||
self._getApiClient().setPrintJobState(print_job_uuid, action)
|
||||
|
||||
def _update(self) -> None:
|
||||
|
@ -106,19 +111,22 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._getApiClient().getPrintJobs(self._updatePrintJobs)
|
||||
self._updatePrintJobPreviewImages()
|
||||
|
||||
## Get a list of materials that are installed on the cluster host.
|
||||
def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None:
|
||||
"""Get a list of materials that are installed on the cluster host."""
|
||||
|
||||
self._getApiClient().getMaterials(on_finished = on_finished)
|
||||
|
||||
## Sync the material profiles in Cura with the printer.
|
||||
# This gets called when connecting to a printer as well as when sending a print.
|
||||
def sendMaterialProfiles(self) -> None:
|
||||
"""Sync the material profiles in Cura with the printer.
|
||||
|
||||
This gets called when connecting to a printer as well as when sending a print.
|
||||
"""
|
||||
job = SendMaterialJob(device = self)
|
||||
job.run()
|
||||
|
||||
## Send a print job to the cluster.
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
"""Send a print job to the cluster."""
|
||||
|
||||
# Show an error message if we're already sending a job.
|
||||
if self._progress.visible:
|
||||
|
@ -132,15 +140,20 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
job.finished.connect(self._onPrintJobCreated)
|
||||
job.start()
|
||||
|
||||
## Allows the user to choose a printer to print with from the printer selection dialogue.
|
||||
# \param unique_name: The unique name of the printer to target.
|
||||
@pyqtSlot(str, name="selectTargetPrinter")
|
||||
def selectTargetPrinter(self, unique_name: str = "") -> None:
|
||||
"""Allows the user to choose a printer to print with from the printer selection dialogue.
|
||||
|
||||
:param unique_name: The unique name of the printer to target.
|
||||
"""
|
||||
self._startPrintJobUpload(unique_name if unique_name != "" else None)
|
||||
|
||||
## Handler for when the print job was created locally.
|
||||
# It can now be sent over the network.
|
||||
def _onPrintJobCreated(self, job: ExportFileJob) -> None:
|
||||
"""Handler for when the print job was created locally.
|
||||
|
||||
It can now be sent over the network.
|
||||
"""
|
||||
|
||||
self._active_exported_job = job
|
||||
# TODO: add preference to enable/disable this feature?
|
||||
if self.clusterSize > 1:
|
||||
|
@ -148,8 +161,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
return
|
||||
self._startPrintJobUpload()
|
||||
|
||||
## Shows a dialog allowing the user to select which printer in a group to send a job to.
|
||||
def _showPrinterSelectionDialog(self) -> None:
|
||||
"""Shows a dialog allowing the user to select which printer in a group to send a job to."""
|
||||
|
||||
if not self._printer_select_dialog:
|
||||
plugin_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or ""
|
||||
path = os.path.join(plugin_path, "resources", "qml", "PrintWindow.qml")
|
||||
|
@ -157,8 +171,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
if self._printer_select_dialog is not None:
|
||||
self._printer_select_dialog.show()
|
||||
|
||||
## Upload the print job to the group.
|
||||
def _startPrintJobUpload(self, unique_name: str = None) -> None:
|
||||
"""Upload the print job to the group."""
|
||||
|
||||
if not self._active_exported_job:
|
||||
Logger.log("e", "No active exported job to upload!")
|
||||
return
|
||||
|
@ -177,33 +192,40 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
on_progress=self._onPrintJobUploadProgress)
|
||||
self._active_exported_job = None
|
||||
|
||||
## Handler for print job upload progress.
|
||||
def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None:
|
||||
"""Handler for print job upload progress."""
|
||||
|
||||
percentage = (bytes_sent / bytes_total) if bytes_total else 0
|
||||
self._progress.setProgress(percentage * 100)
|
||||
self.writeProgress.emit()
|
||||
|
||||
## Handler for when the print job was fully uploaded to the cluster.
|
||||
def _onPrintUploadCompleted(self, _: QNetworkReply) -> None:
|
||||
"""Handler for when the print job was fully uploaded to the cluster."""
|
||||
|
||||
self._progress.hide()
|
||||
PrintJobUploadSuccessMessage().show()
|
||||
self.writeFinished.emit()
|
||||
|
||||
## Displays the given message if uploading the mesh has failed
|
||||
# \param message: The message to display.
|
||||
def _onUploadError(self, message: str = None) -> None:
|
||||
"""Displays the given message if uploading the mesh has failed
|
||||
|
||||
:param message: The message to display.
|
||||
"""
|
||||
|
||||
self._progress.hide()
|
||||
PrintJobUploadErrorMessage(message).show()
|
||||
self.writeError.emit()
|
||||
|
||||
## Download all the images from the cluster and load their data in the print job models.
|
||||
def _updatePrintJobPreviewImages(self):
|
||||
"""Download all the images from the cluster and load their data in the print job models."""
|
||||
|
||||
for print_job in self._print_jobs:
|
||||
if print_job.getPreviewImage() is None:
|
||||
self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData)
|
||||
|
||||
## Get the API client instance.
|
||||
def _getApiClient(self) -> ClusterApiClient:
|
||||
"""Get the API client instance."""
|
||||
|
||||
if not self._cluster_api:
|
||||
self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error)))
|
||||
return self._cluster_api
|
||||
|
|
|
@ -24,8 +24,9 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters.
|
||||
class LocalClusterOutputDeviceManager:
|
||||
"""The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters."""
|
||||
|
||||
|
||||
META_NETWORK_KEY = "um_network_key"
|
||||
|
||||
|
@ -49,30 +50,35 @@ class LocalClusterOutputDeviceManager:
|
|||
self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered)
|
||||
self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved)
|
||||
|
||||
## Start the network discovery.
|
||||
def start(self) -> None:
|
||||
"""Start the network discovery."""
|
||||
|
||||
self._zero_conf_client.start()
|
||||
for address in self._getStoredManualAddresses():
|
||||
self.addManualDevice(address)
|
||||
|
||||
## Stop network discovery and clean up discovered devices.
|
||||
def stop(self) -> None:
|
||||
"""Stop network discovery and clean up discovered devices."""
|
||||
|
||||
self._zero_conf_client.stop()
|
||||
for instance_name in list(self._discovered_devices):
|
||||
self._onDiscoveredDeviceRemoved(instance_name)
|
||||
|
||||
## Restart discovery on the local network.
|
||||
def startDiscovery(self):
|
||||
"""Restart discovery on the local network."""
|
||||
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
## Add a networked printer manually by address.
|
||||
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
|
||||
"""Add a networked printer manually by address."""
|
||||
|
||||
api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error)))
|
||||
api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback))
|
||||
|
||||
## Remove a manually added networked printer.
|
||||
def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None:
|
||||
"""Remove a manually added networked printer."""
|
||||
|
||||
if device_id not in self._discovered_devices and address is not None:
|
||||
device_id = "manual:{}".format(address)
|
||||
|
||||
|
@ -83,16 +89,19 @@ class LocalClusterOutputDeviceManager:
|
|||
if address in self._getStoredManualAddresses():
|
||||
self._removeStoredManualAddress(address)
|
||||
|
||||
## Force reset all network device connections.
|
||||
def refreshConnections(self) -> None:
|
||||
"""Force reset all network device connections."""
|
||||
|
||||
self._connectToActiveMachine()
|
||||
|
||||
## Get the discovered devices.
|
||||
def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]:
|
||||
"""Get the discovered devices."""
|
||||
|
||||
return self._discovered_devices
|
||||
|
||||
## Connect the active machine to a given device.
|
||||
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
|
||||
"""Connect the active machine to a given device."""
|
||||
|
||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
@ -106,8 +115,9 @@ class LocalClusterOutputDeviceManager:
|
|||
return
|
||||
CuraApplication.getInstance().getMachineManager().switchPrinterType(definitions[0].getName())
|
||||
|
||||
## Callback for when the active machine was changed by the user or a new remote cluster was found.
|
||||
def _connectToActiveMachine(self) -> None:
|
||||
"""Callback for when the active machine was changed by the user or a new remote cluster was found."""
|
||||
|
||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
@ -122,9 +132,10 @@ class LocalClusterOutputDeviceManager:
|
|||
# Remove device if it is not meant for the active machine.
|
||||
output_device_manager.removeOutputDevice(device.key)
|
||||
|
||||
## Callback for when a manual device check request was responded to.
|
||||
def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus,
|
||||
callback: Optional[Callable[[bool, str], None]] = None) -> None:
|
||||
"""Callback for when a manual device check request was responded to."""
|
||||
|
||||
self._onDeviceDiscovered("manual:{}".format(address), address, {
|
||||
b"name": status.name.encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
|
@ -137,10 +148,13 @@ class LocalClusterOutputDeviceManager:
|
|||
if callback is not None:
|
||||
CuraApplication.getInstance().callLater(callback, True, address)
|
||||
|
||||
## Returns a dict of printer BOM numbers to machine types.
|
||||
# These numbers are available in the machine definition already so we just search for them here.
|
||||
@staticmethod
|
||||
def _getPrinterTypeIdentifiers() -> Dict[str, str]:
|
||||
"""Returns a dict of printer BOM numbers to machine types.
|
||||
|
||||
These numbers are available in the machine definition already so we just search for them here.
|
||||
"""
|
||||
|
||||
container_registry = CuraApplication.getInstance().getContainerRegistry()
|
||||
ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.")
|
||||
found_machine_type_identifiers = {} # type: Dict[str, str]
|
||||
|
@ -154,8 +168,9 @@ class LocalClusterOutputDeviceManager:
|
|||
found_machine_type_identifiers[str(bom_number)] = machine_type
|
||||
return found_machine_type_identifiers
|
||||
|
||||
## Add a new device.
|
||||
def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None:
|
||||
"""Add a new device."""
|
||||
|
||||
machine_identifier = properties.get(b"machine", b"").decode("utf-8")
|
||||
printer_type_identifiers = self._getPrinterTypeIdentifiers()
|
||||
|
||||
|
@ -189,8 +204,9 @@ class LocalClusterOutputDeviceManager:
|
|||
self.discoveredDevicesChanged.emit()
|
||||
self._connectToActiveMachine()
|
||||
|
||||
## Remove a device.
|
||||
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
|
||||
"""Remove a device."""
|
||||
|
||||
device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice]
|
||||
if not device:
|
||||
return
|
||||
|
@ -198,8 +214,9 @@ class LocalClusterOutputDeviceManager:
|
|||
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address)
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
## Create a machine instance based on the discovered network printer.
|
||||
def _createMachineFromDiscoveredDevice(self, device_id: str) -> None:
|
||||
"""Create a machine instance based on the discovered network printer."""
|
||||
|
||||
device = self._discovered_devices.get(device_id)
|
||||
if device is None:
|
||||
return
|
||||
|
@ -216,8 +233,9 @@ class LocalClusterOutputDeviceManager:
|
|||
self._connectToOutputDevice(device, new_machine)
|
||||
self._showCloudFlowMessage(device)
|
||||
|
||||
## Add an address to the stored preferences.
|
||||
def _storeManualAddress(self, address: str) -> None:
|
||||
"""Add an address to the stored preferences."""
|
||||
|
||||
stored_addresses = self._getStoredManualAddresses()
|
||||
if address in stored_addresses:
|
||||
return # Prevent duplicates.
|
||||
|
@ -225,8 +243,9 @@ class LocalClusterOutputDeviceManager:
|
|||
new_value = ",".join(stored_addresses)
|
||||
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value)
|
||||
|
||||
## Remove an address from the stored preferences.
|
||||
def _removeStoredManualAddress(self, address: str) -> None:
|
||||
"""Remove an address from the stored preferences."""
|
||||
|
||||
stored_addresses = self._getStoredManualAddresses()
|
||||
try:
|
||||
stored_addresses.remove(address) # Can throw a ValueError
|
||||
|
@ -235,15 +254,16 @@ class LocalClusterOutputDeviceManager:
|
|||
except ValueError:
|
||||
Logger.log("w", "Could not remove address from stored_addresses, it was not there")
|
||||
|
||||
## Load the user-configured manual devices from Cura preferences.
|
||||
def _getStoredManualAddresses(self) -> List[str]:
|
||||
"""Load the user-configured manual devices from Cura preferences."""
|
||||
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "")
|
||||
manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",")
|
||||
return manual_instances
|
||||
|
||||
## Add a device to the current active machine.
|
||||
def _connectToOutputDevice(self, device: UltimakerNetworkedPrinterOutputDevice, machine: GlobalStack) -> None:
|
||||
"""Add a device to the current active machine."""
|
||||
|
||||
# Make sure users know that we no longer support legacy devices.
|
||||
if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION:
|
||||
|
@ -262,9 +282,10 @@ class LocalClusterOutputDeviceManager:
|
|||
if device.key not in output_device_manager.getOutputDeviceIds():
|
||||
output_device_manager.addOutputDevice(device)
|
||||
|
||||
## Nudge the user to start using Ultimaker Cloud.
|
||||
@staticmethod
|
||||
def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None:
|
||||
"""Nudge the user to start using Ultimaker Cloud."""
|
||||
|
||||
if CuraApplication.getInstance().getMachineManager().activeMachineHasCloudRegistration:
|
||||
# This printer is already cloud connected, so we do not bother the user anymore.
|
||||
return
|
||||
|
|
|
@ -16,27 +16,33 @@ if TYPE_CHECKING:
|
|||
from .LocalClusterOutputDevice import LocalClusterOutputDevice
|
||||
|
||||
|
||||
## Asynchronous job to send material profiles to the printer.
|
||||
#
|
||||
# This way it won't freeze up the interface while sending those materials.
|
||||
class SendMaterialJob(Job):
|
||||
"""Asynchronous job to send material profiles to the printer.
|
||||
|
||||
This way it won't freeze up the interface while sending those materials.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, device: "LocalClusterOutputDevice") -> None:
|
||||
super().__init__()
|
||||
self.device = device # type: LocalClusterOutputDevice
|
||||
|
||||
## Send the request to the printer and register a callback
|
||||
def run(self) -> None:
|
||||
"""Send the request to the printer and register a callback"""
|
||||
|
||||
self.device.getMaterials(on_finished = self._onGetMaterials)
|
||||
|
||||
## Callback for when the remote materials were returned.
|
||||
def _onGetMaterials(self, materials: List[ClusterMaterial]) -> None:
|
||||
"""Callback for when the remote materials were returned."""
|
||||
|
||||
remote_materials_by_guid = {material.guid: material for material in materials}
|
||||
self._sendMissingMaterials(remote_materials_by_guid)
|
||||
|
||||
## Determine which materials should be updated and send them to the printer.
|
||||
# \param remote_materials_by_guid The remote materials by GUID.
|
||||
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
|
||||
"""Determine which materials should be updated and send them to the printer.
|
||||
|
||||
:param remote_materials_by_guid: The remote materials by GUID.
|
||||
"""
|
||||
local_materials_by_guid = self._getLocalMaterials()
|
||||
if len(local_materials_by_guid) == 0:
|
||||
Logger.log("d", "There are no local materials to synchronize with the printer.")
|
||||
|
@ -47,25 +53,31 @@ class SendMaterialJob(Job):
|
|||
return
|
||||
self._sendMaterials(material_ids_to_send)
|
||||
|
||||
## From the local and remote materials, determine which ones should be synchronized.
|
||||
# Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that
|
||||
# are newer in Cura.
|
||||
# \param local_materials The local materials by GUID.
|
||||
# \param remote_materials The remote materials by GUID.
|
||||
@staticmethod
|
||||
def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial],
|
||||
remote_materials: Dict[str, ClusterMaterial]) -> Set[str]:
|
||||
"""From the local and remote materials, determine which ones should be synchronized.
|
||||
|
||||
Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that
|
||||
are newer in Cura.
|
||||
:param local_materials: The local materials by GUID.
|
||||
:param remote_materials: The remote materials by GUID.
|
||||
"""
|
||||
|
||||
return {
|
||||
local_material.id
|
||||
for guid, local_material in local_materials.items()
|
||||
if guid not in remote_materials.keys() or local_material.version > remote_materials[guid].version
|
||||
}
|
||||
|
||||
## Send the materials to the printer.
|
||||
# The given materials will be loaded from disk en sent to to printer.
|
||||
# The given id's will be matched with filenames of the locally stored materials.
|
||||
# \param materials_to_send A set with id's of materials that must be sent.
|
||||
def _sendMaterials(self, materials_to_send: Set[str]) -> None:
|
||||
"""Send the materials to the printer.
|
||||
|
||||
The given materials will be loaded from disk en sent to to printer.
|
||||
The given id's will be matched with filenames of the locally stored materials.
|
||||
:param materials_to_send: A set with id's of materials that must be sent.
|
||||
"""
|
||||
|
||||
container_registry = CuraApplication.getInstance().getContainerRegistry()
|
||||
all_materials = container_registry.findInstanceContainersMetadata(type = "material")
|
||||
all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material).
|
||||
|
@ -83,12 +95,14 @@ class SendMaterialJob(Job):
|
|||
file_name = os.path.basename(file_path)
|
||||
self._sendMaterialFile(file_path, file_name, root_material_id)
|
||||
|
||||
## Send a single material file to the printer.
|
||||
# Also add the material signature file if that is available.
|
||||
# \param file_path The path of the material file.
|
||||
# \param file_name The name of the material file.
|
||||
# \param material_id The ID of the material in the file.
|
||||
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
|
||||
"""Send a single material file to the printer.
|
||||
|
||||
Also add the material signature file if that is available.
|
||||
:param file_path: The path of the material file.
|
||||
:param file_name: The name of the material file.
|
||||
:param material_id: The ID of the material in the file.
|
||||
"""
|
||||
parts = []
|
||||
|
||||
# Add the material file.
|
||||
|
@ -112,8 +126,9 @@ class SendMaterialJob(Job):
|
|||
self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts,
|
||||
on_finished = self._sendingFinished)
|
||||
|
||||
## Check a reply from an upload to the printer and log an error when the call failed
|
||||
def _sendingFinished(self, reply: QNetworkReply) -> None:
|
||||
"""Check a reply from an upload to the printer and log an error when the call failed"""
|
||||
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
Logger.log("w", "Error while syncing material: %s", reply.errorString())
|
||||
return
|
||||
|
@ -125,11 +140,14 @@ class SendMaterialJob(Job):
|
|||
# Because of the guards above it is not shown when syncing failed (which is not always an actual problem).
|
||||
MaterialSyncMessage(self.device).show()
|
||||
|
||||
## Retrieves a list of local materials
|
||||
# Only the new newest version of the local materials is returned
|
||||
# \return a dictionary of LocalMaterial objects by GUID
|
||||
@staticmethod
|
||||
def _getLocalMaterials() -> Dict[str, LocalMaterial]:
|
||||
"""Retrieves a list of local materials
|
||||
|
||||
Only the new newest version of the local materials is returned
|
||||
:return: a dictionary of LocalMaterial objects by GUID
|
||||
"""
|
||||
|
||||
result = {} # type: Dict[str, LocalMaterial]
|
||||
all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material")
|
||||
all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent.
|
||||
|
|
|
@ -12,9 +12,11 @@ from UM.Signal import Signal
|
|||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The ZeroConfClient handles all network discovery logic.
|
||||
# It emits signals when new network services were found or disappeared.
|
||||
class ZeroConfClient:
|
||||
"""The ZeroConfClient handles all network discovery logic.
|
||||
|
||||
It emits signals when new network services were found or disappeared.
|
||||
"""
|
||||
|
||||
# The discovery protocol name for Ultimaker printers.
|
||||
ZERO_CONF_NAME = u"_ultimaker._tcp.local."
|
||||
|
@ -30,10 +32,13 @@ class ZeroConfClient:
|
|||
self._service_changed_request_event = None # type: Optional[Event]
|
||||
self._service_changed_request_thread = None # type: Optional[Thread]
|
||||
|
||||
## The ZeroConf service changed requests are handled in a separate thread so we don't block the UI.
|
||||
# We can also re-schedule the requests when they fail to get detailed service info.
|
||||
# Any new or re-reschedule requests will be appended to the request queue and the thread will process them.
|
||||
def start(self) -> None:
|
||||
"""The ZeroConf service changed requests are handled in a separate thread so we don't block the UI.
|
||||
|
||||
We can also re-schedule the requests when they fail to get detailed service info.
|
||||
Any new or re-reschedule requests will be appended to the request queue and the thread will process them.
|
||||
"""
|
||||
|
||||
self._service_changed_request_queue = Queue()
|
||||
self._service_changed_request_event = Event()
|
||||
try:
|
||||
|
@ -56,16 +61,18 @@ class ZeroConfClient:
|
|||
self._zero_conf_browser.cancel()
|
||||
self._zero_conf_browser = None
|
||||
|
||||
## Handles a change is discovered network services.
|
||||
def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None:
|
||||
"""Handles a change is discovered network services."""
|
||||
|
||||
item = (zeroconf, service_type, name, state_change)
|
||||
if not self._service_changed_request_queue or not self._service_changed_request_event:
|
||||
return
|
||||
self._service_changed_request_queue.put(item)
|
||||
self._service_changed_request_event.set()
|
||||
|
||||
## Callback for when a ZeroConf service has changes.
|
||||
def _handleOnServiceChangedRequests(self) -> None:
|
||||
"""Callback for when a ZeroConf service has changes."""
|
||||
|
||||
if not self._service_changed_request_queue or not self._service_changed_request_event:
|
||||
return
|
||||
|
||||
|
@ -98,19 +105,23 @@ class ZeroConfClient:
|
|||
for request in reschedule_requests:
|
||||
self._service_changed_request_queue.put(request)
|
||||
|
||||
## Handler for zeroConf detection.
|
||||
# Return True or False indicating if the process succeeded.
|
||||
# Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread.
|
||||
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange
|
||||
) -> bool:
|
||||
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str,
|
||||
state_change: ServiceStateChange) -> bool:
|
||||
"""Handler for zeroConf detection.
|
||||
|
||||
Return True or False indicating if the process succeeded.
|
||||
Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread.
|
||||
"""
|
||||
|
||||
if state_change == ServiceStateChange.Added:
|
||||
return self._onServiceAdded(zero_conf, service_type, name)
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
return self._onServiceRemoved(name)
|
||||
return True
|
||||
|
||||
## Handler for when a ZeroConf service was added.
|
||||
def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool:
|
||||
"""Handler for when a ZeroConf service was added."""
|
||||
|
||||
# First try getting info from zero-conf cache
|
||||
info = ServiceInfo(service_type, name, properties={})
|
||||
for record in zero_conf.cache.entries_with_name(name.lower()):
|
||||
|
@ -141,8 +152,9 @@ class ZeroConfClient:
|
|||
|
||||
return True
|
||||
|
||||
## Handler for when a ZeroConf service was removed.
|
||||
def _onServiceRemoved(self, name: str) -> bool:
|
||||
"""Handler for when a ZeroConf service was removed."""
|
||||
|
||||
Logger.log("d", "ZeroConf service removed: %s" % name)
|
||||
self.removedNetworkCluster.emit(str(name))
|
||||
return True
|
||||
|
|
|
@ -13,11 +13,11 @@ from .Network.LocalClusterOutputDeviceManager import LocalClusterOutputDeviceMan
|
|||
from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
|
||||
|
||||
|
||||
## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing.
|
||||
class UM3OutputDevicePlugin(OutputDevicePlugin):
|
||||
|
||||
# Signal emitted when the list of discovered devices changed. Used by printer action in this plugin.
|
||||
"""This plugin handles the discovery and networking for Ultimaker 3D printers"""
|
||||
|
||||
discoveredDevicesChanged = Signal()
|
||||
"""Signal emitted when the list of discovered devices changed. Used by printer action in this plugin."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
@ -33,8 +33,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
|
|||
# This ensures no output devices are still connected that do not belong to the new active machine.
|
||||
CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections)
|
||||
|
||||
## Start looking for devices in the network and cloud.
|
||||
def start(self):
|
||||
"""Start looking for devices in the network and cloud."""
|
||||
|
||||
self._network_output_device_manager.start()
|
||||
self._cloud_output_device_manager.start()
|
||||
|
||||
|
@ -43,31 +44,38 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
|
|||
self._network_output_device_manager.stop()
|
||||
self._cloud_output_device_manager.stop()
|
||||
|
||||
## Restart network discovery.
|
||||
def startDiscovery(self) -> None:
|
||||
"""Restart network discovery."""
|
||||
|
||||
self._network_output_device_manager.startDiscovery()
|
||||
|
||||
## Force refreshing the network connections.
|
||||
def refreshConnections(self) -> None:
|
||||
"""Force refreshing the network connections."""
|
||||
|
||||
self._network_output_device_manager.refreshConnections()
|
||||
self._cloud_output_device_manager.refreshConnections()
|
||||
|
||||
## Indicate that this plugin supports adding networked printers manually.
|
||||
def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt:
|
||||
"""Indicate that this plugin supports adding networked printers manually."""
|
||||
|
||||
return ManualDeviceAdditionAttempt.PRIORITY
|
||||
|
||||
## Add a networked printer manually based on its network address.
|
||||
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
|
||||
"""Add a networked printer manually based on its network address."""
|
||||
|
||||
self._network_output_device_manager.addManualDevice(address, callback)
|
||||
|
||||
## Remove a manually connected networked printer.
|
||||
def removeManualDevice(self, key: str, address: Optional[str] = None) -> None:
|
||||
"""Remove a manually connected networked printer."""
|
||||
|
||||
self._network_output_device_manager.removeManualDevice(key, address)
|
||||
|
||||
## Get the discovered devices from the local network.
|
||||
|
||||
def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]:
|
||||
"""Get the discovered devices from the local network."""
|
||||
|
||||
return self._network_output_device_manager.getDiscoveredDevices()
|
||||
|
||||
## Connect the active machine to a device.
|
||||
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
|
||||
"""Connect the active machine to a device."""
|
||||
|
||||
self._network_output_device_manager.associateActiveMachineWithPrinterDevice(device)
|
||||
|
|
|
@ -15,9 +15,11 @@ from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Machine action that allows to connect the active machine to a networked devices.
|
||||
# TODO: in the future this should be part of the new discovery workflow baked into Cura.
|
||||
class UltimakerNetworkedPrinterAction(MachineAction):
|
||||
"""Machine action that allows to connect the active machine to a networked devices.
|
||||
|
||||
TODO: in the future this should be part of the new discovery workflow baked into Cura.
|
||||
"""
|
||||
|
||||
# Signal emitted when discovered devices have changed.
|
||||
discoveredDevicesChanged = pyqtSignal()
|
||||
|
@ -27,59 +29,69 @@ class UltimakerNetworkedPrinterAction(MachineAction):
|
|||
self._qml_url = "resources/qml/DiscoverUM3Action.qml"
|
||||
self._network_plugin = None # type: Optional[UM3OutputDevicePlugin]
|
||||
|
||||
## Override the default value.
|
||||
def needsUserInteraction(self) -> bool:
|
||||
"""Override the default value."""
|
||||
|
||||
return False
|
||||
|
||||
## Start listening to network discovery events via the plugin.
|
||||
@pyqtSlot(name = "startDiscovery")
|
||||
def startDiscovery(self) -> None:
|
||||
"""Start listening to network discovery events via the plugin."""
|
||||
|
||||
self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
|
||||
self.discoveredDevicesChanged.emit() # trigger at least once to populate the list
|
||||
|
||||
## Reset the discovered devices.
|
||||
@pyqtSlot(name = "reset")
|
||||
def reset(self) -> None:
|
||||
"""Reset the discovered devices."""
|
||||
|
||||
self.discoveredDevicesChanged.emit() # trigger to reset the list
|
||||
|
||||
## Reset the discovered devices.
|
||||
@pyqtSlot(name = "restartDiscovery")
|
||||
def restartDiscovery(self) -> None:
|
||||
"""Reset the discovered devices."""
|
||||
|
||||
self._networkPlugin.startDiscovery()
|
||||
self.discoveredDevicesChanged.emit() # trigger to reset the list
|
||||
|
||||
## Remove a manually added device.
|
||||
@pyqtSlot(str, str, name = "removeManualDevice")
|
||||
def removeManualDevice(self, key: str, address: str) -> None:
|
||||
"""Remove a manually added device."""
|
||||
|
||||
self._networkPlugin.removeManualDevice(key, address)
|
||||
|
||||
## Add a new manual device. Can replace an existing one by key.
|
||||
@pyqtSlot(str, str, name = "setManualDevice")
|
||||
def setManualDevice(self, key: str, address: str) -> None:
|
||||
"""Add a new manual device. Can replace an existing one by key."""
|
||||
|
||||
if key != "":
|
||||
self._networkPlugin.removeManualDevice(key)
|
||||
if address != "":
|
||||
self._networkPlugin.addManualDevice(address)
|
||||
|
||||
## Get the devices discovered in the local network sorted by name.
|
||||
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
|
||||
def foundDevices(self):
|
||||
"""Get the devices discovered in the local network sorted by name."""
|
||||
|
||||
discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values())
|
||||
discovered_devices.sort(key = lambda d: d.name)
|
||||
return discovered_devices
|
||||
|
||||
## Connect a device selected in the list with the active machine.
|
||||
@pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice")
|
||||
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
|
||||
"""Connect a device selected in the list with the active machine."""
|
||||
|
||||
self._networkPlugin.associateActiveMachineWithPrinterDevice(device)
|
||||
|
||||
## Callback for when the list of discovered devices in the plugin was changed.
|
||||
def _onDeviceDiscoveryChanged(self) -> None:
|
||||
"""Callback for when the list of discovered devices in the plugin was changed."""
|
||||
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
## Get the network manager from the plugin.
|
||||
@property
|
||||
def _networkPlugin(self) -> UM3OutputDevicePlugin:
|
||||
"""Get the network manager from the plugin."""
|
||||
|
||||
if not self._network_plugin:
|
||||
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||
network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting")
|
||||
|
|
|
@ -22,10 +22,12 @@ from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus
|
|||
from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
|
||||
|
||||
|
||||
## Output device class that forms the basis of Ultimaker networked printer output devices.
|
||||
# Currently used for local networking and cloud printing using Ultimaker Connect.
|
||||
# This base class primarily contains all the Qt properties and slots needed for the monitor page to work.
|
||||
class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
||||
"""Output device class that forms the basis of Ultimaker networked printer output devices.
|
||||
|
||||
Currently used for local networking and cloud printing using Ultimaker Connect.
|
||||
This base class primarily contains all the Qt properties and slots needed for the monitor page to work.
|
||||
"""
|
||||
|
||||
META_NETWORK_KEY = "um_network_key"
|
||||
META_CLUSTER_ID = "um_cloud_cluster_id"
|
||||
|
@ -85,14 +87,16 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
# The job upload progress message modal.
|
||||
self._progress = PrintJobUploadProgressMessage()
|
||||
|
||||
## The IP address of the printer.
|
||||
@pyqtProperty(str, constant=True)
|
||||
def address(self) -> str:
|
||||
"""The IP address of the printer."""
|
||||
|
||||
return self._address
|
||||
|
||||
## The display name of the printer.
|
||||
@pyqtProperty(str, constant=True)
|
||||
def printerTypeName(self) -> str:
|
||||
"""The display name of the printer."""
|
||||
|
||||
return self._printer_type_name
|
||||
|
||||
# Get all print jobs for this cluster.
|
||||
|
@ -157,13 +161,15 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
self._active_printer = printer
|
||||
self.activePrinterChanged.emit()
|
||||
|
||||
## Whether the printer that this output device represents supports print job actions via the local network.
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def supportsPrintJobActions(self) -> bool:
|
||||
"""Whether the printer that this output device represents supports print job actions via the local network."""
|
||||
|
||||
return True
|
||||
|
||||
## Set the remote print job state.
|
||||
def setJobState(self, print_job_uuid: str, state: str) -> None:
|
||||
"""Set the remote print job state."""
|
||||
|
||||
raise NotImplementedError("setJobState must be implemented")
|
||||
|
||||
@pyqtSlot(str, name="sendJobToTop")
|
||||
|
@ -210,11 +216,13 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
self._checkStillConnected()
|
||||
super()._update()
|
||||
|
||||
## Check if we're still connected by comparing the last timestamps for network response and the current time.
|
||||
# This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly.
|
||||
# Re-connecting is handled automatically by the output device managers in this plugin.
|
||||
# TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes.
|
||||
def _checkStillConnected(self) -> None:
|
||||
"""Check if we're still connected by comparing the last timestamps for network response and the current time.
|
||||
|
||||
This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly.
|
||||
Re-connecting is handled automatically by the output device managers in this plugin.
|
||||
TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes.
|
||||
"""
|
||||
time_since_last_response = time() - self._time_of_last_response
|
||||
if time_since_last_response > self.NETWORK_RESPONSE_CONSIDER_OFFLINE:
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
@ -223,9 +231,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
elif self.connectionState == ConnectionState.Closed:
|
||||
self._reconnectForActiveMachine()
|
||||
|
||||
## Reconnect for the active output device.
|
||||
# Does nothing if the device is not meant for the active machine.
|
||||
def _reconnectForActiveMachine(self) -> None:
|
||||
"""Reconnect for the active output device.
|
||||
|
||||
Does nothing if the device is not meant for the active machine.
|
||||
"""
|
||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
@ -281,16 +291,19 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
self.printersChanged.emit()
|
||||
self._checkIfClusterHost()
|
||||
|
||||
## Check is this device is a cluster host and takes the needed actions when it is not.
|
||||
def _checkIfClusterHost(self):
|
||||
"""Check is this device is a cluster host and takes the needed actions when it is not."""
|
||||
|
||||
if len(self._printers) < 1 and self.isConnected():
|
||||
NotClusterHostMessage(self).show()
|
||||
self.close()
|
||||
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(self.key)
|
||||
|
||||
## Updates the local list of print jobs with the list received from the cluster.
|
||||
# \param remote_jobs: The print jobs received from the cluster.
|
||||
def _updatePrintJobs(self, remote_jobs: List[ClusterPrintJobStatus]) -> None:
|
||||
"""Updates the local list of print jobs with the list received from the cluster.
|
||||
|
||||
:param remote_jobs: The print jobs received from the cluster.
|
||||
"""
|
||||
self._responseReceived()
|
||||
|
||||
# Keep track of the new print jobs to show.
|
||||
|
@ -321,9 +334,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
self._print_jobs = new_print_jobs
|
||||
self.printJobsChanged.emit()
|
||||
|
||||
## Create a new print job model based on the remote status of the job.
|
||||
# \param remote_job: The remote print job data.
|
||||
def _createPrintJobModel(self, remote_job: ClusterPrintJobStatus) -> UM3PrintJobOutputModel:
|
||||
"""Create a new print job model based on the remote status of the job.
|
||||
|
||||
:param remote_job: The remote print job data.
|
||||
"""
|
||||
model = remote_job.createOutputModel(ClusterOutputController(self))
|
||||
if remote_job.printer_uuid:
|
||||
self._updateAssignedPrinter(model, remote_job.printer_uuid)
|
||||
|
@ -333,16 +348,18 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
|
|||
model.loadPreviewImageFromUrl(remote_job.preview_url)
|
||||
return model
|
||||
|
||||
## Updates the printer assignment for the given print job model.
|
||||
def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None:
|
||||
"""Updates the printer assignment for the given print job model."""
|
||||
|
||||
printer = next((p for p in self._printers if printer_uuid == p.key), None)
|
||||
if not printer:
|
||||
return
|
||||
printer.updateActivePrintJob(model)
|
||||
model.updateAssignedPrinter(printer)
|
||||
|
||||
## Load Monitor tab QML.
|
||||
def _loadMonitorTab(self) -> None:
|
||||
"""Load Monitor tab QML."""
|
||||
|
||||
plugin_registry = CuraApplication.getInstance().getPluginRegistry()
|
||||
if not plugin_registry:
|
||||
Logger.log("e", "Could not get plugin registry")
|
||||
|
|
|
@ -110,20 +110,22 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
application = CuraApplication.getInstance()
|
||||
application.triggerNextExitCheck()
|
||||
|
||||
## Reset USB device settings
|
||||
#
|
||||
def resetDeviceSettings(self) -> None:
|
||||
"""Reset USB device settings"""
|
||||
|
||||
self._firmware_name = None
|
||||
|
||||
## Request the current scene to be sent to a USB-connected printer.
|
||||
#
|
||||
# \param nodes A collection of scene nodes to send. This is ignored.
|
||||
# \param file_name A suggestion for a file name to write.
|
||||
# \param filter_by_machine Whether to filter MIME types by machine. This
|
||||
# is ignored.
|
||||
# \param kwargs Keyword arguments.
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
"""Request the current scene to be sent to a USB-connected printer.
|
||||
|
||||
:param nodes: A collection of scene nodes to send. This is ignored.
|
||||
:param file_name: A suggestion for a file name to write.
|
||||
:param filter_by_machine: Whether to filter MIME types by machine. This
|
||||
is ignored.
|
||||
:param kwargs: Keyword arguments.
|
||||
"""
|
||||
|
||||
if self._is_printing:
|
||||
message = Message(text = catalog.i18nc("@message", "A print is still in progress. Cura cannot start another print via USB until the previous print has completed."), title = catalog.i18nc("@message", "Print in Progress"))
|
||||
message.show()
|
||||
|
@ -144,9 +146,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
self._printGCode(gcode_textio.getvalue())
|
||||
|
||||
## Start a print based on a g-code.
|
||||
# \param gcode The g-code to print.
|
||||
def _printGCode(self, gcode: str):
|
||||
"""Start a print based on a g-code.
|
||||
|
||||
:param gcode: The g-code to print.
|
||||
"""
|
||||
self._gcode.clear()
|
||||
self._paused = False
|
||||
|
||||
|
@ -219,8 +223,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._update_thread = Thread(target=self._update, daemon=True, name = "USBPrinterUpdate")
|
||||
self._serial = None
|
||||
|
||||
## Send a command to printer.
|
||||
def sendCommand(self, command: Union[str, bytes]):
|
||||
"""Send a command to printer."""
|
||||
|
||||
if not self._command_received.is_set():
|
||||
self._command_queue.put(command)
|
||||
else:
|
||||
|
|
|
@ -20,9 +20,10 @@ from . import USBPrinterOutputDevice
|
|||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
|
||||
@signalemitter
|
||||
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
||||
"""Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer."""
|
||||
|
||||
addUSBOutputDeviceSignal = Signal()
|
||||
progressChanged = pyqtSignal()
|
||||
|
||||
|
@ -85,8 +86,9 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
|||
self._addRemovePorts(port_list)
|
||||
time.sleep(5)
|
||||
|
||||
## Helper to identify serial ports (and scan for them)
|
||||
def _addRemovePorts(self, serial_ports):
|
||||
"""Helper to identify serial ports (and scan for them)"""
|
||||
|
||||
# First, find and add all new or changed keys
|
||||
for serial_port in list(serial_ports):
|
||||
if serial_port not in self._serial_port_list:
|
||||
|
@ -98,16 +100,19 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
|||
if port not in self._serial_port_list:
|
||||
device.close()
|
||||
|
||||
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
def addOutputDevice(self, serial_port):
|
||||
"""Because the model needs to be created in the same thread as the QMLEngine, we use a signal."""
|
||||
|
||||
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
|
||||
device.connectionStateChanged.connect(self._onConnectionStateChanged)
|
||||
self._usb_output_devices[serial_port] = device
|
||||
device.connect()
|
||||
|
||||
## Create a list of serial ports on the system.
|
||||
# \param only_list_usb If true, only usb ports are listed
|
||||
def getSerialPortList(self, only_list_usb = False):
|
||||
"""Create a list of serial ports on the system.
|
||||
|
||||
:param only_list_usb: If true, only usb ports are listed
|
||||
"""
|
||||
base_list = []
|
||||
for port in serial.tools.list_ports.comports():
|
||||
if not isinstance(port, tuple):
|
||||
|
|
|
@ -31,7 +31,7 @@ def readHex(filename):
|
|||
check_sum &= 0xFF
|
||||
if check_sum != 0:
|
||||
raise Exception("Checksum error in hex file: " + line)
|
||||
|
||||
|
||||
if rec_type == 0:#Data record
|
||||
while len(data) < addr + rec_len:
|
||||
data.append(0)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue