Convert doxygen to rst for UM3NetworkPrinting

This commit is contained in:
Nino van Hooff 2020-05-15 15:05:38 +02:00
parent de82406782
commit 5eb5ffd916
38 changed files with 797 additions and 487 deletions

View file

@ -20,13 +20,15 @@ from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
from ..Models.Http.CloudPrintResponse import CloudPrintResponse from ..Models.Http.CloudPrintResponse import CloudPrintResponse
## The generic type variable used to document the methods below.
CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel) 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: 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. # The cloud URL to use for this remote cluster.
ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
@ -36,54 +38,70 @@ class CloudApiClient:
# In order to avoid garbage collection we keep the callbacks in this list. # In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[], None]] _anti_gc_callbacks = [] # type: List[Callable[[], 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, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> 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.
"""
super().__init__() super().__init__()
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._account = account self._account = account
self._on_error = on_error self._on_error = on_error
self._upload = None # type: Optional[ToolPathUploader] self._upload = None # type: Optional[ToolPathUploader]
## Gets the account used for the API.
@property @property
def account(self) -> Account: def account(self) -> Account:
"""Gets the account used for the API."""
return self._account 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: 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) url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, CloudClusterResponse, failed) self._addCallback(reply, on_finished, CloudClusterResponse, failed)
## 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: 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) url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, CloudClusterStatus) self._addCallback(reply, on_finished, CloudClusterStatus)
## 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, def requestUpload(self, request: CloudPrintJobUploadRequest,
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: 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) url = "{}/jobs/upload".format(self.CURA_API_ROOT)
body = json.dumps({"data": request.toDict()}) body = json.dumps({"data": request.toDict()})
reply = self._manager.put(self._createEmptyRequest(url), body.encode()) reply = self._manager.put(self._createEmptyRequest(url), body.encode())
self._addCallback(reply, on_finished, CloudPrintJobResponse) self._addCallback(reply, on_finished, CloudPrintJobResponse)
## 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], def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
on_progress: Callable[[int], Any], on_error: 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._manager, print_job, mesh, on_finished, on_progress, on_error) self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error)
self._upload.start() self._upload.start()
@ -96,20 +114,27 @@ class CloudApiClient:
reply = self._manager.post(self._createEmptyRequest(url), b"") reply = self._manager.post(self._createEmptyRequest(url), b"")
self._addCallback(reply, on_finished, CloudPrintResponse) self._addCallback(reply, on_finished, CloudPrintResponse)
## 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, def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,
data: Optional[Dict[str, Any]] = None) -> None: 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"" 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) url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
self._manager.post(self._createEmptyRequest(url), body) self._manager.post(self._createEmptyRequest(url), body)
## 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: 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)) request = QNetworkRequest(QUrl(path))
if content_type: if content_type:
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
@ -118,11 +143,14 @@ class CloudApiClient:
request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode())
return request 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 @staticmethod
def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: 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) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
try: try:
response = bytes(reply.readAll()).decode() response = bytes(reply.readAll()).decode()
@ -133,14 +161,15 @@ class CloudApiClient:
Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
return status_code, {"errors": [error.toDict()]} return status_code, {"errors": [error.toDict()]}
## Parses the given models and calls the correct callback depending on the result. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
# \param response: The response from the server, after being converted to a dict. Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None:
# \param on_finished: The callback in case the response is successful. """Parses the given models and calls the correct callback depending on the result.
# \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], :param response: The response from the server, after being converted to a dict.
on_finished: Union[Callable[[CloudApiClientModel], Any], :param on_finished: The callback in case the response is successful.
Callable[[List[CloudApiClientModel]], Any]], :param model_class: The type of the model to convert the response to. It may either be a single record or a list.
model_class: Type[CloudApiClientModel]) -> None: """
if "data" in response: if "data" in response:
data = response["data"] data = response["data"]
if isinstance(data, list): if isinstance(data, list):
@ -156,18 +185,21 @@ class CloudApiClient:
else: else:
Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) 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 _addCallback(self, def _addCallback(self,
reply: QNetworkReply, reply: QNetworkReply,
on_finished: Union[Callable[[CloudApiClientModel], Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
Callable[[List[CloudApiClientModel]], Any]], Callable[[List[CloudApiClientModel]], Any]],
model: Type[CloudApiClientModel], model: Type[CloudApiClientModel],
on_error: Optional[Callable] = None) -> None: on_error: Optional[Callable] = 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. 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() -> None: def parse() -> None:
self._anti_gc_callbacks.remove(parse) self._anti_gc_callbacks.remove(parse)

View file

@ -35,11 +35,13 @@ from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
I18N_CATALOG = i18nCatalog("cura") 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): 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. # The interval with which the remote cluster is checked.
# We can do this relatively often as this API call is quite fast. # 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. # Therefore we create a private signal used to trigger the printersChanged signal.
_cloudClusterPrintersChanged = pyqtSignal() _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: 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. # 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. # 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._tool_path = None # type: Optional[bytes]
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
## Connects this device.
def connect(self) -> None: def connect(self) -> None:
"""Connects this device."""
if self.isConnected(): if self.isConnected():
return return
super().connect() super().connect()
@ -108,21 +113,24 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange) CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
self._update() self._update()
## Disconnects the device
def disconnect(self) -> None: def disconnect(self) -> None:
"""Disconnects the device"""
if not self.isConnected(): if not self.isConnected():
return return
super().disconnect() super().disconnect()
Logger.log("i", "Disconnected from cluster %s", self.key) Logger.log("i", "Disconnected from cluster %s", self.key)
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange) 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: 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._tool_path = None
self._uploaded_print_job = 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: 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." # Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
# the host name should then be "ultimakersystem-aabbccdd0011" # the host name should then be "ultimakersystem-aabbccdd0011"
if network_key.startswith(str(self.clusterData.host_name or "")): if network_key.startswith(str(self.clusterData.host_name or "")):
@ -133,15 +141,17 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return True return True
return False return False
## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None: 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.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.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
## Called when the network data should be updated.
def _update(self) -> None: def _update(self) -> None:
"""Called when the network data should be updated."""
super()._update() super()._update()
if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL: if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
return # avoid calling the cloud too often return # avoid calling the cloud too often
@ -153,9 +163,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
else: else:
self.setAuthenticationState(AuthState.NotAuthenticated) 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: 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() self._responseReceived()
if status.printers != self._received_printers: if status.printers != self._received_printers:
self._received_printers = status.printers self._received_printers = status.printers
@ -164,10 +176,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._received_print_jobs = status.print_jobs self._received_print_jobs = status.print_jobs
self._updatePrintJobs(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, 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: 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. # Show an error message if we're already sending a job.
if self._progress.visible: if self._progress.visible:
PrintJobUploadBlockedMessage().show() PrintJobUploadBlockedMessage().show()
@ -187,9 +200,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
job.finished.connect(self._onPrintJobCreated) job.finished.connect(self._onPrintJobCreated)
job.start() 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: 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() output = job.getOutput()
self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again
file_name = job.getFileName() file_name = job.getFileName()
@ -200,9 +215,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
) )
self._api.requestUpload(request, self._uploadPrintJob) 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: 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: if not self._tool_path:
return self._onUploadError() return self._onUploadError()
self._progress.show() self._progress.show()
@ -210,38 +227,45 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update, self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
self._onUploadError) self._onUploadError)
## Requests the print to be sent to the printer when we finished uploading the mesh.
def _onPrintJobUploaded(self) -> None: def _onPrintJobUploaded(self) -> None:
"""Requests the print to be sent to the printer when we finished uploading the mesh."""
self._progress.update(100) self._progress.update(100)
print_job = cast(CloudPrintJobResponse, self._uploaded_print_job) print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted) 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: 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() self._progress.hide()
PrintJobUploadSuccessMessage().show() PrintJobUploadSuccessMessage().show()
self.writeFinished.emit() 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: 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._progress.hide()
self._uploaded_print_job = None self._uploaded_print_job = None
PrintJobUploadErrorMessage(message).show() PrintJobUploadErrorMessage(message).show()
self.writeError.emit() self.writeError.emit()
## Whether the printer that this output device represents supports print job actions via the cloud.
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged) @pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
def supportsPrintJobActions(self) -> bool: def supportsPrintJobActions(self) -> bool:
"""Whether the printer that this output device represents supports print job actions via the cloud."""
if not self._printers: if not self._printers:
return False return False
version_number = self.printers[0].firmwareVersion.split(".") version_number = self.printers[0].firmwareVersion.split(".")
firmware_version = Version([version_number[0], version_number[1], version_number[2]]) firmware_version = Version([version_number[0], version_number[1], version_number[2]])
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION 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: 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) self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state)
@pyqtSlot(str, name="sendJobToTop") @pyqtSlot(str, name="sendJobToTop")
@ -265,18 +289,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
def openPrinterControlPanel(self) -> None: def openPrinterControlPanel(self) -> None:
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl)) QDesktopServices.openUrl(QUrl(self.clusterCloudUrl))
## Gets the cluster response from which this device was created.
@property @property
def clusterData(self) -> CloudClusterResponse: def clusterData(self) -> CloudClusterResponse:
"""Gets the cluster response from which this device was created."""
return self._cluster return self._cluster
## Updates the cluster data from the cloud.
@clusterData.setter @clusterData.setter
def clusterData(self, value: CloudClusterResponse) -> None: def clusterData(self, value: CloudClusterResponse) -> None:
"""Updates the cluster data from the cloud."""
self._cluster = value self._cluster = value
## Gets the URL on which to monitor the cluster via the cloud.
@property @property
def clusterCloudUrl(self) -> str: 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 "" 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) return "https://mycloud{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id)

View file

@ -10,8 +10,9 @@ from UM.Logger import Logger
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
## Class responsible for uploading meshes to the cloud in separate requests.
class ToolPathUploader: 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 # The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES
MAX_RETRIES = 10 MAX_RETRIES = 10
@ -22,16 +23,19 @@ class ToolPathUploader:
# The amount of bytes to send per request # The amount of bytes to send per request
BYTES_PER_REQUEST = 256 * 1024 BYTES_PER_REQUEST = 256 * 1024
## 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.
def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes, def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes,
on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any] on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]
) -> None: ) -> 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._manager = manager self._manager = manager
self._print_job = print_job self._print_job = print_job
self._data = data self._data = data
@ -45,13 +49,15 @@ class ToolPathUploader:
self._finished = False self._finished = False
self._reply = None # type: Optional[QNetworkReply] self._reply = None # type: Optional[QNetworkReply]
## Returns the print job for which this object was created.
@property @property
def printJob(self): def printJob(self):
"""Returns the print job for which this object was created."""
return self._print_job return self._print_job
## Creates a network request to the print job upload URL, adding the needed content range header.
def _createRequest(self) -> QNetworkRequest: def _createRequest(self) -> QNetworkRequest:
"""Creates a network request to the print job upload URL, adding the needed content range header."""
request = QNetworkRequest(QUrl(self._print_job.upload_url)) request = QNetworkRequest(QUrl(self._print_job.upload_url))
request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type) request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type)
@ -62,14 +68,17 @@ class ToolPathUploader:
return request return request
## 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]: 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) last_byte = min(len(self._data), self._sent_bytes + self.BYTES_PER_REQUEST)
return self._sent_bytes, last_byte return self._sent_bytes, last_byte
## Starts uploading the mesh.
def start(self) -> None: def start(self) -> None:
"""Starts uploading the mesh."""
if self._finished: if self._finished:
# reset state. # reset state.
self._sent_bytes = 0 self._sent_bytes = 0
@ -77,13 +86,15 @@ class ToolPathUploader:
self._finished = False self._finished = False
self._uploadChunk() self._uploadChunk()
## Stops uploading the mesh, marking it as finished.
def stop(self): def stop(self):
"""Stops uploading the mesh, marking it as finished."""
Logger.log("i", "Stopped uploading") Logger.log("i", "Stopped uploading")
self._finished = True self._finished = True
## Uploads a chunk of the mesh to the cloud.
def _uploadChunk(self) -> None: def _uploadChunk(self) -> None:
"""Uploads a chunk of the mesh to the cloud."""
if self._finished: if self._finished:
raise ValueError("The upload is already finished") raise ValueError("The upload is already finished")
@ -96,25 +107,29 @@ class ToolPathUploader:
self._reply.uploadProgress.connect(self._progressCallback) self._reply.uploadProgress.connect(self._progressCallback)
self._reply.error.connect(self._errorCallback) self._reply.error.connect(self._errorCallback)
## 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: 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) Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total)
if bytes_total: if bytes_total:
total_sent = self._sent_bytes + bytes_sent total_sent = self._sent_bytes + bytes_sent
self._on_progress(int(total_sent / len(self._data) * 100)) self._on_progress(int(total_sent / len(self._data) * 100))
## Handles an error uploading.
def _errorCallback(self) -> None: def _errorCallback(self) -> None:
"""Handles an error uploading."""
reply = cast(QNetworkReply, self._reply) reply = cast(QNetworkReply, self._reply)
body = bytes(reply.readAll()).decode() body = bytes(reply.readAll()).decode()
Logger.log("e", "Received error while uploading: %s", body) Logger.log("e", "Received error while uploading: %s", body)
self.stop() self.stop()
self._on_error() self._on_error()
## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
def _finishedCallback(self) -> None: def _finishedCallback(self) -> None:
"""Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed."""
reply = cast(QNetworkReply, self._reply) reply = cast(QNetworkReply, self._reply)
Logger.log("i", "Finished callback %s %s", Logger.log("i", "Finished callback %s %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString()) reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
@ -140,8 +155,9 @@ class ToolPathUploader:
[bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode()) [bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode())
self._chunkUploaded() self._chunkUploaded()
## Handles a chunk of data being uploaded, starting the next chunk if needed.
def _chunkUploaded(self) -> None: 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. # We got a successful response. Let's start the next chunk or report the upload is finished.
first_byte, last_byte = self._chunkRange() first_byte, last_byte = self._chunkRange()
self._sent_bytes += last_byte - first_byte self._sent_bytes += last_byte - first_byte

View file

@ -9,8 +9,8 @@ from cura.CuraApplication import CuraApplication
from .MeshFormatHandler import MeshFormatHandler from .MeshFormatHandler import MeshFormatHandler
## Job that exports the build plate to the correct file format for the target cluster.
class ExportFileJob(WriteFileJob): 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: 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", "") extension = self._mesh_format_handler.preferred_format.get("extension", "")
self.setFileName("{}.{}".format(job_name, extension)) self.setFileName("{}.{}".format(job_name, extension))
## Get the mime type of the selected export file type.
def getMimeType(self) -> str: def getMimeType(self) -> str:
"""Get the mime type of the selected export file type."""
return self._mesh_format_handler.mime_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: 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() output = self.getStream().getvalue()
if isinstance(output, str): if isinstance(output, str):
output = output.encode("utf-8") output = output.encode("utf-8")

View file

@ -16,8 +16,9 @@ from cura.CuraApplication import CuraApplication
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## This class is responsible for choosing the formats used by the connected clusters.
class MeshFormatHandler: 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: def __init__(self, file_handler: Optional[FileHandler], firmware_version: str) -> None:
self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler() self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler()
@ -28,42 +29,50 @@ class MeshFormatHandler:
def is_valid(self) -> bool: def is_valid(self) -> bool:
return bool(self._writer) 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 @property
def preferred_format(self) -> Dict[str, Union[str, int, bool]]: 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 return self._preferred_format
## Gets the file writer for the given file handler and mime type.
# \return A file writer.
@property @property
def writer(self) -> Optional[FileWriter]: def writer(self) -> Optional[FileWriter]:
"""Gets the file writer for the given file handler and mime type.
:return: A file writer.
"""
return self._writer return self._writer
@property @property
def mime_type(self) -> str: def mime_type(self) -> str:
return cast(str, self._preferred_format["mime_type"]) return cast(str, self._preferred_format["mime_type"])
## Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)
@property @property
def file_mode(self) -> int: def file_mode(self) -> int:
"""Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)"""
return cast(int, self._preferred_format["mode"]) return cast(int, self._preferred_format["mode"])
## Gets the file extension
@property @property
def file_extension(self) -> str: def file_extension(self) -> str:
"""Gets the file extension"""
return cast(str, self._preferred_format["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]: 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: if self.file_mode == FileWriter.OutputMode.TextMode:
return io.StringIO() return io.StringIO()
else: else:
return io.BytesIO() return io.BytesIO()
## Writes the mesh and returns its value.
def getBytes(self, nodes: List[SceneNode]) -> bytes: def getBytes(self, nodes: List[SceneNode]) -> bytes:
"""Writes the mesh and returns its value."""
if self.writer is None: if self.writer is None:
raise ValueError("There is no writer for the mesh format handler.") raise ValueError("There is no writer for the mesh format handler.")
stream = self.createStream() stream = self.createStream()
@ -73,10 +82,12 @@ class MeshFormatHandler:
value = value.encode() value = value.encode()
return value 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]]: 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). # Formats supported by this application (file types that we can actually write).
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
@ -108,9 +119,11 @@ class MeshFormatHandler:
) )
return file_formats[0] 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]: 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. # Just take the first file format available.
return self._file_handler.getWriterByMimeType(mime_type) return self._file_handler.getWriterByMimeType(mime_type)

View file

@ -7,12 +7,12 @@ from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## Message shown when trying to connect to a legacy printer device.
class LegacyDeviceNoLongerSupportedMessage(Message): class LegacyDeviceNoLongerSupportedMessage(Message):
"""Message shown when trying to connect to a legacy printer device."""
# Singleton used to prevent duplicate messages of this type at the same time.
__is_visible = False __is_visible = False
"""Singleton used to prevent duplicate messages of this type at the same time."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not " text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not "

View file

@ -13,11 +13,11 @@ if TYPE_CHECKING:
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## Message shown when sending material files to cluster host.
class MaterialSyncMessage(Message): 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 __is_visible = False
"""Singleton used to prevent duplicate messages of this type at the same time."""
def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None: def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None:
super().__init__( super().__init__(

View file

@ -16,11 +16,11 @@ if TYPE_CHECKING:
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## Message shown when trying to connect to a printer that is not a host.
class NotClusterHostMessage(Message): 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 __is_visible = False
"""Singleton used to prevent duplicate messages of this type at the same time."""
def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None: def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None:
super().__init__( super().__init__(

View file

@ -7,9 +7,9 @@ from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura") 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): 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: def __init__(self) -> None:
super().__init__( super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), text = I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."),

View file

@ -7,9 +7,9 @@ from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## Message shown when uploading a print job to a cluster failed.
class PrintJobUploadErrorMessage(Message): class PrintJobUploadErrorMessage(Message):
"""Message shown when uploading a print job to a cluster failed."""
def __init__(self, message: str = None) -> None: def __init__(self, message: str = None) -> None:
super().__init__( super().__init__(
text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."), text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),

View file

@ -7,8 +7,9 @@ from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## Class responsible for showing a progress message while a mesh is being uploaded to the cloud.
class PrintJobUploadProgressMessage(Message): class PrintJobUploadProgressMessage(Message):
"""Class responsible for showing a progress message while a mesh is being uploaded to the cloud."""
def __init__(self): def __init__(self):
super().__init__( super().__init__(
title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"), title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"),
@ -19,14 +20,17 @@ class PrintJobUploadProgressMessage(Message):
use_inactivity_timer = False use_inactivity_timer = False
) )
## Shows the progress message.
def show(self): def show(self):
"""Shows the progress message."""
self.setProgress(0) self.setProgress(0)
super().show() super().show()
## Updates the percentage of the uploaded.
# \param percentage: The percentage amount (0-100).
def update(self, percentage: int) -> None: def update(self, percentage: int) -> None:
"""Updates the percentage of the uploaded.
:param percentage: The percentage amount (0-100).
"""
if not self._visible: if not self._visible:
super().show() super().show()
self.setProgress(percentage) self.setProgress(percentage)

View file

@ -7,9 +7,9 @@ from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## Message shown when uploading a print job to a cluster succeeded.
class PrintJobUploadSuccessMessage(Message): class PrintJobUploadSuccessMessage(Message):
"""Message shown when uploading a print job to a cluster succeeded."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),

View file

@ -18,45 +18,56 @@ class BaseModel:
def validate(self) -> None: def validate(self) -> None:
pass 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): 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() 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: 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() return type(self) != type(other) or self.toDict() != other.toDict()
## Converts the model into a serializable dictionary
def toDict(self) -> Dict[str, Any]: def toDict(self) -> Dict[str, Any]:
"""Converts the model into a serializable dictionary"""
return self.__dict__ 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 @staticmethod
def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T: 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): if isinstance(values, dict):
return model_class(**values) return model_class(**values)
return 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 @classmethod
def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]: 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] 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 @staticmethod
def parseDate(date: Union[str, datetime]) -> datetime: 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): if isinstance(date, datetime):
return date return date
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc) return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)

View file

@ -5,22 +5,26 @@ from typing import Optional
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing a cloud connected cluster.
class CloudClusterResponse(BaseModel): 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, 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, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None,
friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", **kwargs) -> 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.cluster_id = cluster_id
self.host_guid = host_guid self.host_guid = host_guid
self.host_name = host_name self.host_name = host_name

View file

@ -11,15 +11,17 @@ from .ClusterPrintJobStatus import ClusterPrintJobStatus
# Model that represents the status of the cluster for the cloud # Model that represents the status of the cluster for the cloud
class CloudClusterStatus(BaseModel): class CloudClusterStatus(BaseModel):
## Creates a new cluster status model object. def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]],
# \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]]],
print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]],
generated_time: Union[str, datetime], generated_time: Union[str, datetime],
**kwargs) -> None: **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.generated_time = self.parseDate(generated_time)
self.printers = self.parseModels(ClusterPrinterStatus, printers) self.printers = self.parseModels(ClusterPrinterStatus, printers)
self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs)

View file

@ -5,20 +5,23 @@ from typing import Dict, Optional, Any
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing errors generated by the cloud servers, according to the JSON-API standard.
class CloudError(BaseModel): 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, def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None,
meta: Optional[Dict[str, Any]] = None, **kwargs) -> 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.id = id
self.code = code self.code = code
self.http_status = http_status self.http_status = http_status

View file

@ -8,19 +8,22 @@ from ..BaseModel import BaseModel
# Model that represents the response received from the cloud after requesting to upload a print job # Model that represents the response received from the cloud after requesting to upload a print job
class CloudPrintJobResponse(BaseModel): 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, 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, upload_url: Optional[str] = None, content_type: Optional[str] = None,
status_description: Optional[str] = None, slicing_details: Optional[dict] = None, **kwargs) -> 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.job_id = job_id
self.status = status self.status = status
self.download_url = download_url self.download_url = download_url

View file

@ -6,11 +6,14 @@ from ..BaseModel import BaseModel
# Model that represents the request to upload a print job to the cloud # Model that represents the request to upload a print job to the cloud
class CloudPrintJobUploadRequest(BaseModel): 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: 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.job_name = job_name
self.file_size = file_size self.file_size = file_size
self.content_type = content_type self.content_type = content_type

View file

@ -9,13 +9,16 @@ from ..BaseModel import BaseModel
# Model that represents the responses received from the cloud after requesting a job to be printed. # Model that represents the responses received from the cloud after requesting a job to be printed.
class CloudPrintResponse(BaseModel): 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], def __init__(self, job_id: str, status: str, generated_time: Union[str, datetime],
cluster_job_id: Optional[str] = None, **kwargs) -> None: 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.job_id = job_id
self.status = status self.status = status
self.cluster_job_id = cluster_job_id self.cluster_job_id = cluster_job_id

View file

@ -3,11 +3,13 @@
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing a cluster printer
class ClusterBuildPlate(BaseModel): 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: 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 self.type = type
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -9,26 +9,33 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate
from ..BaseModel import BaseModel 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 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.extruder_index = extruder_index
self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None
self.print_core_id = print_core_id self.print_core_id = print_core_id
super().__init__(**kwargs) super().__init__(**kwargs)
## Updates the given output model.
# \param model - The output model to update.
def updateOutputModel(self, model: ExtruderOutputModel) -> None: 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: if self.print_core_id is not None:
model.updateHotendID(self.print_core_id) model.updateHotendID(self.print_core_id)
@ -40,14 +47,16 @@ class ClusterPrintCoreConfiguration(BaseModel):
else: else:
model.updateActiveMaterial(None) model.updateActiveMaterial(None)
## Creates a configuration model
def createConfigurationModel(self) -> ExtruderConfigurationModel: def createConfigurationModel(self) -> ExtruderConfigurationModel:
"""Creates a configuration model"""
model = ExtruderConfigurationModel(position = self.extruder_index) model = ExtruderConfigurationModel(position = self.extruder_index)
self.updateConfigurationModel(model) self.updateConfigurationModel(model)
return model return model
## Creates a configuration model
def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel: def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel:
"""Creates a configuration model"""
model.setHotendID(self.print_core_id) model.setHotendID(self.print_core_id)
if self.material: if self.material:
model.setMaterial(self.material.createOutputModel()) model.setMaterial(self.material.createOutputModel())

View file

@ -5,19 +5,22 @@ from typing import Optional
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Model for the types of changes that are needed before a print job can start
class ClusterPrintJobConfigurationChange(BaseModel): 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.type_of_change = type_of_change
self.index = index self.index = index
self.target_id = target_id self.target_id = target_id

View file

@ -5,12 +5,14 @@ from typing import Optional
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing a cloud cluster print job constraint
class ClusterPrintJobConstraints(BaseModel): 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: 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 self.require_printer_name = require_printer_name
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -3,14 +3,17 @@
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing the reasons that prevent this job from being printed on the associated printer
class ClusterPrintJobImpediment(BaseModel): 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: 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.translation_key = translation_key
self.severity = severity self.severity = severity
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -15,36 +15,9 @@ from ..BaseModel import BaseModel
from ...ClusterOutputController import ClusterOutputController from ...ClusterOutputController import ClusterOutputController
## Model for the status of a single print job in a cluster.
class ClusterPrintJobStatus(BaseModel): 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, def __init__(self, created_at: str, force: bool, machine_variant: str, name: str, started: bool, status: str,
time_total: int, uuid: str, time_total: int, uuid: str,
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]],
@ -60,6 +33,37 @@ class ClusterPrintJobStatus(BaseModel):
impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None, impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None,
preview_url: Optional[str] = None, preview_url: Optional[str] = None,
**kwargs) -> 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.assigned_to = assigned_to
self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
self.constraints = self.parseModels(ClusterPrintJobConstraints, constraints) self.constraints = self.parseModels(ClusterPrintJobConstraints, constraints)
@ -90,24 +94,31 @@ class ClusterPrintJobStatus(BaseModel):
super().__init__(**kwargs) 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: 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) model = UM3PrintJobOutputModel(controller, self.uuid, self.name)
self.updateOutputModel(model) self.updateOutputModel(model)
return model return model
## Creates a new configuration model
def _createConfigurationModel(self) -> PrinterConfigurationModel: def _createConfigurationModel(self) -> PrinterConfigurationModel:
"""Creates a new configuration model"""
extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()] extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
configuration = PrinterConfigurationModel() configuration = PrinterConfigurationModel()
configuration.setExtruderConfigurations(extruders) configuration.setExtruderConfigurations(extruders)
configuration.setPrinterType(self.machine_variant) configuration.setPrinterType(self.machine_variant)
return configuration 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: 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.updateConfiguration(self._createConfigurationModel())
model.updateTimeTotal(self.time_total) model.updateTimeTotal(self.time_total)
model.updateTimeElapsed(self.time_elapsed) model.updateTimeElapsed(self.time_elapsed)

View file

@ -9,29 +9,35 @@ from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing a cloud cluster printer configuration
class ClusterPrinterConfigurationMaterial(BaseModel): 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, def __init__(self, brand: Optional[str] = None, color: Optional[str] = None, guid: Optional[str] = None,
material: Optional[str] = None, **kwargs) -> 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.guid = guid
self.brand = brand self.brand = brand
self.color = color self.color = color
self.material = material self.material = material
super().__init__(**kwargs) 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: 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() container_registry = ContainerRegistry.getInstance()
same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid) same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid)
if same_guid: if same_guid:

View file

@ -6,16 +6,19 @@ from ..BaseModel import BaseModel
from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot
## Class representing the data of a Material Station in the cluster.
class ClusterPrinterMaterialStation(BaseModel): 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, def __init__(self, status: str, supported: bool = False,
material_slots: List[Union[ClusterPrinterMaterialStationSlot, Dict[str, Any]]] = None, material_slots: List[Union[ClusterPrinterMaterialStationSlot, Dict[str, Any]]] = None,
**kwargs) -> 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.status = status
self.supported = supported self.supported = supported
self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\ self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\

View file

@ -5,16 +5,19 @@ from typing import Optional
from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
## Class representing the data of a single slot in the material station.
class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration): class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration):
"""Class representing the data of a single slot in the material station."""
## 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.
def __init__(self, slot_index: int, compatible: bool, material_remaining: float, def __init__(self, slot_index: int, compatible: bool, material_remaining: float,
material_empty: Optional[bool] = False, **kwargs) -> None: 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.slot_index = slot_index
self.compatible = compatible self.compatible = compatible
self.material_remaining = material_remaining self.material_remaining = material_remaining

View file

@ -17,26 +17,10 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing a cluster printer
class ClusterPrinterStatus(BaseModel): 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, def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
status: str, unique_name: str, uuid: str, status: str, unique_name: str, uuid: str,
configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], 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, firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None,
build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None,
material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> 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.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration)
self.enabled = enabled self.enabled = enabled
@ -63,9 +66,12 @@ class ClusterPrinterStatus(BaseModel):
material_station) if material_station else None material_station) if material_station else None
super().__init__(**kwargs) super().__init__(**kwargs)
## Creates a new output model.
# \param controller - The controller of the model.
def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel: def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel:
"""Creates a new output model.
:param controller: - The controller of the model.
"""
# FIXME # FIXME
# Note that we're using '2' here as extruder count. We have hardcoded this for now to prevent issues where the # 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 # 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) self.updateOutputModel(model)
return model return model
## Updates the given output model.
# \param model - The output model to update.
def updateOutputModel(self, model: PrinterOutputModel) -> None: def updateOutputModel(self, model: PrinterOutputModel) -> None:
"""Updates the given output model.
:param model: - The output model to update.
"""
model.updateKey(self.uuid) model.updateKey(self.uuid)
model.updateName(self.friendly_name) model.updateName(self.friendly_name)
model.updateUniqueName(self.unique_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))] ) for left_slot, right_slot in product(self._getSlotsForExtruder(0), self._getSlotsForExtruder(1))]
model.setAvailableConfigurations(available_configurations) 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]: 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 if not self.material_station: # typing guard
return [] return []
slots = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( 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)] 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 @staticmethod
def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool: 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 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 @staticmethod
def _createEmptyMaterialSlot(extruder_index: int) -> ClusterPrinterMaterialStationSlot: def _createEmptyMaterialSlot(extruder_index: int) -> ClusterPrinterMaterialStationSlot:
"""Create an empty material slot with a fake empty material."""
empty_material = ClusterPrinterConfigurationMaterial(guid = "", material = "empty", brand = "", color = "") empty_material = ClusterPrinterConfigurationMaterial(guid = "", material = "empty", brand = "", color = "")
return ClusterPrinterMaterialStationSlot(slot_index = 0, extruder_index = extruder_index, return ClusterPrinterMaterialStationSlot(slot_index = 0, extruder_index = extruder_index,
compatible = True, material_remaining = 0, material = empty_material) compatible = True, material_remaining = 0, material = empty_material)

View file

@ -5,12 +5,11 @@ from typing import Dict, Any
from ..BaseModel import BaseModel from ..BaseModel import BaseModel
## Class representing the system status of a printer.
class PrinterSystemStatus(BaseModel): 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, def __init__(self, guid: str, firmware: str, hostname: str, name: str, platform: str, variant: str,
hardware: Dict[str, Any], **kwargs hardware: Dict[str, Any], **kwargs) -> None:
) -> None:
self.guid = guid self.guid = guid
self.firmware = firmware self.firmware = firmware
self.hostname = hostname self.hostname = hostname

View file

@ -16,12 +16,13 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus
from ..Models.Http.ClusterMaterial import ClusterMaterial from ..Models.Http.ClusterMaterial import ClusterMaterial
## The generic type variable used to document the methods below.
ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel) 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: class ClusterApiClient:
"""The ClusterApiClient is responsible for all network calls to local network clusters."""
PRINTER_API_PREFIX = "/api/v1" PRINTER_API_PREFIX = "/api/v1"
CLUSTER_API_PREFIX = "/cluster-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. # In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[], None]] _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: 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__() super().__init__()
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._address = address self._address = address
self._on_error = on_error 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: 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) url = "{}/system".format(self.PRINTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, PrinterSystemStatus) 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: 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) url = "{}/materials".format(self.CLUSTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, ClusterMaterial) 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: 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) url = "{}/printers".format(self.CLUSTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, ClusterPrinterStatus) 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: 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) url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, ClusterPrintJobStatus) self._addCallback(reply, on_finished, ClusterPrintJobStatus)
## Move a print job to the top of the queue.
def movePrintJobToTop(self, print_job_uuid: str) -> None: 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) 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()) 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: 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) url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode()) 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: 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) url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.deleteResource(self._createEmptyRequest(url)) self._manager.deleteResource(self._createEmptyRequest(url))
## Set the state of a print job.
def setPrintJobState(self, print_job_uuid: str, state: str) -> None: 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) 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. # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints.
action = "print" if state == "resume" else state action = "print" if state == "resume" else state
self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) 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: 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) url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid)
reply = self._manager.get(self._createEmptyRequest(url)) reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished) 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: 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) url = QUrl("http://" + self._address + path)
request = QNetworkRequest(url) request = QNetworkRequest(url)
request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
@ -105,11 +123,13 @@ class ClusterApiClient:
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
return request 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 @staticmethod
def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: 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) status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
try: try:
response = bytes(reply.readAll()).decode() response = bytes(reply.readAll()).decode()
@ -118,14 +138,15 @@ class ClusterApiClient:
Logger.logException("e", "Could not parse the cluster response: %s", err) Logger.logException("e", "Could not parse the cluster response: %s", err)
return status_code, {"errors": [err]} return status_code, {"errors": [err]}
## Parses the given models and calls the correct callback depending on the result. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any],
# \param response: The response from the server, after being converted to a dict. Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None:
# \param on_finished: The callback in case the response is successful. """Parses the given models and calls the correct callback depending on the result.
# \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], :param response: The response from the server, after being converted to a dict.
on_finished: Union[Callable[[ClusterApiClientModel], Any], :param on_finished: The callback in case the response is successful.
Callable[[List[ClusterApiClientModel]], Any]], :param model_class: The type of the model to convert the response to. It may either be a single record or a list.
model_class: Type[ClusterApiClientModel]) -> None: """
try: try:
if isinstance(response, list): if isinstance(response, list):
results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel] results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel]
@ -138,16 +159,15 @@ class ClusterApiClient:
except (JSONDecodeError, TypeError, ValueError): except (JSONDecodeError, TypeError, ValueError):
Logger.log("e", "Could not parse response from network: %s", str(response)) 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. def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any],
# The callback is added to the 'finished' signal of the reply. Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None,
# \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,
) -> 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: def parse() -> None:
self._anti_gc_callbacks.remove(parse) self._anti_gc_callbacks.remove(parse)

View file

@ -51,15 +51,17 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._setInterfaceElements() self._setInterfaceElements()
self._active_camera_url = QUrl() # type: QUrl self._active_camera_url = QUrl() # type: QUrl
## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None: 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.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.setShortDescription(I18N_CATALOG.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network"))
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network"))
## Called when the connection to the cluster changes.
def connect(self) -> None: def connect(self) -> None:
"""Called when the connection to the cluster changes."""
super().connect() super().connect()
self._update() self._update()
self.sendMaterialProfiles() self.sendMaterialProfiles()
@ -94,10 +96,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
def forceSendJob(self, print_job_uuid: str) -> None: def forceSendJob(self, print_job_uuid: str) -> None:
self._getApiClient().forcePrintJob(print_job_uuid) 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: 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) self._getApiClient().setPrintJobState(print_job_uuid, action)
def _update(self) -> None: def _update(self) -> None:
@ -106,19 +111,22 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._getApiClient().getPrintJobs(self._updatePrintJobs) self._getApiClient().getPrintJobs(self._updatePrintJobs)
self._updatePrintJobPreviewImages() self._updatePrintJobPreviewImages()
## Get a list of materials that are installed on the cluster host.
def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None: 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) 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: 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 = SendMaterialJob(device = self)
job.run() job.run()
## Send a print job to the cluster.
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, 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: 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. # Show an error message if we're already sending a job.
if self._progress.visible: if self._progress.visible:
@ -132,15 +140,20 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
job.finished.connect(self._onPrintJobCreated) job.finished.connect(self._onPrintJobCreated)
job.start() 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") @pyqtSlot(str, name="selectTargetPrinter")
def selectTargetPrinter(self, unique_name: str = "") -> None: 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) 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: 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 self._active_exported_job = job
# TODO: add preference to enable/disable this feature? # TODO: add preference to enable/disable this feature?
if self.clusterSize > 1: if self.clusterSize > 1:
@ -148,8 +161,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return return
self._startPrintJobUpload() self._startPrintJobUpload()
## Shows a dialog allowing the user to select which printer in a group to send a job to.
def _showPrinterSelectionDialog(self) -> None: 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: if not self._printer_select_dialog:
plugin_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "" plugin_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or ""
path = os.path.join(plugin_path, "resources", "qml", "PrintWindow.qml") 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: if self._printer_select_dialog is not None:
self._printer_select_dialog.show() self._printer_select_dialog.show()
## Upload the print job to the group.
def _startPrintJobUpload(self, unique_name: str = None) -> None: def _startPrintJobUpload(self, unique_name: str = None) -> None:
"""Upload the print job to the group."""
if not self._active_exported_job: if not self._active_exported_job:
Logger.log("e", "No active exported job to upload!") Logger.log("e", "No active exported job to upload!")
return return
@ -177,33 +192,40 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
on_progress=self._onPrintJobUploadProgress) on_progress=self._onPrintJobUploadProgress)
self._active_exported_job = None self._active_exported_job = None
## Handler for print job upload progress.
def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None: 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 percentage = (bytes_sent / bytes_total) if bytes_total else 0
self._progress.setProgress(percentage * 100) self._progress.setProgress(percentage * 100)
self.writeProgress.emit() self.writeProgress.emit()
## Handler for when the print job was fully uploaded to the cluster.
def _onPrintUploadCompleted(self, _: QNetworkReply) -> None: def _onPrintUploadCompleted(self, _: QNetworkReply) -> None:
"""Handler for when the print job was fully uploaded to the cluster."""
self._progress.hide() self._progress.hide()
PrintJobUploadSuccessMessage().show() PrintJobUploadSuccessMessage().show()
self.writeFinished.emit() 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: 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._progress.hide()
PrintJobUploadErrorMessage(message).show() PrintJobUploadErrorMessage(message).show()
self.writeError.emit() self.writeError.emit()
## Download all the images from the cluster and load their data in the print job models.
def _updatePrintJobPreviewImages(self): 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: for print_job in self._print_jobs:
if print_job.getPreviewImage() is None: if print_job.getPreviewImage() is None:
self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData) self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData)
## Get the API client instance.
def _getApiClient(self) -> ClusterApiClient: def _getApiClient(self) -> ClusterApiClient:
"""Get the API client instance."""
if not self._cluster_api: if not self._cluster_api:
self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error))) self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error)))
return self._cluster_api return self._cluster_api

View file

@ -24,8 +24,9 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus
I18N_CATALOG = i18nCatalog("cura") I18N_CATALOG = i18nCatalog("cura")
## The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters.
class LocalClusterOutputDeviceManager: class LocalClusterOutputDeviceManager:
"""The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters."""
META_NETWORK_KEY = "um_network_key" META_NETWORK_KEY = "um_network_key"
@ -49,30 +50,35 @@ class LocalClusterOutputDeviceManager:
self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered) self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered)
self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved) self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved)
## Start the network discovery.
def start(self) -> None: def start(self) -> None:
"""Start the network discovery."""
self._zero_conf_client.start() self._zero_conf_client.start()
for address in self._getStoredManualAddresses(): for address in self._getStoredManualAddresses():
self.addManualDevice(address) self.addManualDevice(address)
## Stop network discovery and clean up discovered devices.
def stop(self) -> None: def stop(self) -> None:
"""Stop network discovery and clean up discovered devices."""
self._zero_conf_client.stop() self._zero_conf_client.stop()
for instance_name in list(self._discovered_devices): for instance_name in list(self._discovered_devices):
self._onDiscoveredDeviceRemoved(instance_name) self._onDiscoveredDeviceRemoved(instance_name)
## Restart discovery on the local network.
def startDiscovery(self): def startDiscovery(self):
"""Restart discovery on the local network."""
self.stop() self.stop()
self.start() self.start()
## Add a networked printer manually by address.
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: 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 = ClusterApiClient(address, lambda error: Logger.log("e", str(error)))
api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback)) 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: 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: if device_id not in self._discovered_devices and address is not None:
device_id = "manual:{}".format(address) device_id = "manual:{}".format(address)
@ -83,16 +89,19 @@ class LocalClusterOutputDeviceManager:
if address in self._getStoredManualAddresses(): if address in self._getStoredManualAddresses():
self._removeStoredManualAddress(address) self._removeStoredManualAddress(address)
## Force reset all network device connections.
def refreshConnections(self) -> None: def refreshConnections(self) -> None:
"""Force reset all network device connections."""
self._connectToActiveMachine() self._connectToActiveMachine()
## Get the discovered devices.
def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]:
"""Get the discovered devices."""
return self._discovered_devices return self._discovered_devices
## Connect the active machine to a given device.
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
"""Connect the active machine to a given device."""
active_machine = CuraApplication.getInstance().getGlobalContainerStack() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine: if not active_machine:
return return
@ -106,8 +115,9 @@ class LocalClusterOutputDeviceManager:
return return
CuraApplication.getInstance().getMachineManager().switchPrinterType(definitions[0].getName()) 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: 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() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine: if not active_machine:
return return
@ -122,9 +132,10 @@ class LocalClusterOutputDeviceManager:
# Remove device if it is not meant for the active machine. # Remove device if it is not meant for the active machine.
output_device_manager.removeOutputDevice(device.key) output_device_manager.removeOutputDevice(device.key)
## Callback for when a manual device check request was responded to.
def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus, def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus,
callback: Optional[Callable[[bool, str], None]] = None) -> None: 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, { self._onDeviceDiscovered("manual:{}".format(address), address, {
b"name": status.name.encode("utf-8"), b"name": status.name.encode("utf-8"),
b"address": address.encode("utf-8"), b"address": address.encode("utf-8"),
@ -137,10 +148,13 @@ class LocalClusterOutputDeviceManager:
if callback is not None: if callback is not None:
CuraApplication.getInstance().callLater(callback, True, address) 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 @staticmethod
def _getPrinterTypeIdentifiers() -> Dict[str, str]: 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() container_registry = CuraApplication.getInstance().getContainerRegistry()
ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.") ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.")
found_machine_type_identifiers = {} # type: Dict[str, str] found_machine_type_identifiers = {} # type: Dict[str, str]
@ -154,8 +168,9 @@ class LocalClusterOutputDeviceManager:
found_machine_type_identifiers[str(bom_number)] = machine_type found_machine_type_identifiers[str(bom_number)] = machine_type
return found_machine_type_identifiers return found_machine_type_identifiers
## Add a new device.
def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: 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") machine_identifier = properties.get(b"machine", b"").decode("utf-8")
printer_type_identifiers = self._getPrinterTypeIdentifiers() printer_type_identifiers = self._getPrinterTypeIdentifiers()
@ -189,8 +204,9 @@ class LocalClusterOutputDeviceManager:
self.discoveredDevicesChanged.emit() self.discoveredDevicesChanged.emit()
self._connectToActiveMachine() self._connectToActiveMachine()
## Remove a device.
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
"""Remove a device."""
device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice] device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice]
if not device: if not device:
return return
@ -198,8 +214,9 @@ class LocalClusterOutputDeviceManager:
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address)
self.discoveredDevicesChanged.emit() self.discoveredDevicesChanged.emit()
## Create a machine instance based on the discovered network printer.
def _createMachineFromDiscoveredDevice(self, device_id: str) -> None: def _createMachineFromDiscoveredDevice(self, device_id: str) -> None:
"""Create a machine instance based on the discovered network printer."""
device = self._discovered_devices.get(device_id) device = self._discovered_devices.get(device_id)
if device is None: if device is None:
return return
@ -216,8 +233,9 @@ class LocalClusterOutputDeviceManager:
self._connectToOutputDevice(device, new_machine) self._connectToOutputDevice(device, new_machine)
self._showCloudFlowMessage(device) self._showCloudFlowMessage(device)
## Add an address to the stored preferences.
def _storeManualAddress(self, address: str) -> None: def _storeManualAddress(self, address: str) -> None:
"""Add an address to the stored preferences."""
stored_addresses = self._getStoredManualAddresses() stored_addresses = self._getStoredManualAddresses()
if address in stored_addresses: if address in stored_addresses:
return # Prevent duplicates. return # Prevent duplicates.
@ -225,8 +243,9 @@ class LocalClusterOutputDeviceManager:
new_value = ",".join(stored_addresses) new_value = ",".join(stored_addresses)
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value)
## Remove an address from the stored preferences.
def _removeStoredManualAddress(self, address: str) -> None: def _removeStoredManualAddress(self, address: str) -> None:
"""Remove an address from the stored preferences."""
stored_addresses = self._getStoredManualAddresses() stored_addresses = self._getStoredManualAddresses()
try: try:
stored_addresses.remove(address) # Can throw a ValueError stored_addresses.remove(address) # Can throw a ValueError
@ -235,15 +254,16 @@ class LocalClusterOutputDeviceManager:
except ValueError: except ValueError:
Logger.log("w", "Could not remove address from stored_addresses, it was not there") 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]: def _getStoredManualAddresses(self) -> List[str]:
"""Load the user-configured manual devices from Cura preferences."""
preferences = CuraApplication.getInstance().getPreferences() preferences = CuraApplication.getInstance().getPreferences()
preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "")
manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",")
return manual_instances return manual_instances
## Add a device to the current active machine.
def _connectToOutputDevice(self, device: UltimakerNetworkedPrinterOutputDevice, machine: GlobalStack) -> None: 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. # Make sure users know that we no longer support legacy devices.
if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION: if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION:
@ -262,9 +282,10 @@ class LocalClusterOutputDeviceManager:
if device.key not in output_device_manager.getOutputDeviceIds(): if device.key not in output_device_manager.getOutputDeviceIds():
output_device_manager.addOutputDevice(device) output_device_manager.addOutputDevice(device)
## Nudge the user to start using Ultimaker Cloud.
@staticmethod @staticmethod
def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None: def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None:
"""Nudge the user to start using Ultimaker Cloud."""
if CuraApplication.getInstance().getMachineManager().activeMachineIsUsingCloudConnection: if CuraApplication.getInstance().getMachineManager().activeMachineIsUsingCloudConnection:
# This printer is already cloud connected, so we do not bother the user anymore. # This printer is already cloud connected, so we do not bother the user anymore.
return return

View file

@ -16,27 +16,33 @@ if TYPE_CHECKING:
from .LocalClusterOutputDevice import LocalClusterOutputDevice 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): 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: def __init__(self, device: "LocalClusterOutputDevice") -> None:
super().__init__() super().__init__()
self.device = device # type: LocalClusterOutputDevice self.device = device # type: LocalClusterOutputDevice
## Send the request to the printer and register a callback
def run(self) -> None: def run(self) -> None:
"""Send the request to the printer and register a callback"""
self.device.getMaterials(on_finished = self._onGetMaterials) self.device.getMaterials(on_finished = self._onGetMaterials)
## Callback for when the remote materials were returned.
def _onGetMaterials(self, materials: List[ClusterMaterial]) -> None: 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} remote_materials_by_guid = {material.guid: material for material in materials}
self._sendMissingMaterials(remote_materials_by_guid) 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: 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() local_materials_by_guid = self._getLocalMaterials()
if len(local_materials_by_guid) == 0: if len(local_materials_by_guid) == 0:
Logger.log("d", "There are no local materials to synchronize with the printer.") Logger.log("d", "There are no local materials to synchronize with the printer.")
@ -47,25 +53,31 @@ class SendMaterialJob(Job):
return return
self._sendMaterials(material_ids_to_send) 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 @staticmethod
def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial], def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial],
remote_materials: Dict[str, ClusterMaterial]) -> Set[str]: 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 { return {
local_material.id local_material.id
for guid, local_material in local_materials.items() for guid, local_material in local_materials.items()
if guid not in remote_materials.keys() or local_material.version > remote_materials[guid].version 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: 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() container_registry = CuraApplication.getInstance().getContainerRegistry()
all_materials = container_registry.findInstanceContainersMetadata(type = "material") 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). 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) file_name = os.path.basename(file_path)
self._sendMaterialFile(file_path, file_name, root_material_id) 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: 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 = [] parts = []
# Add the material file. # Add the material file.
@ -112,8 +126,9 @@ class SendMaterialJob(Job):
self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts, self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts,
on_finished = self._sendingFinished) 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: 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: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("w", "Error while syncing material: %s", reply.errorString()) Logger.log("w", "Error while syncing material: %s", reply.errorString())
return 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). # Because of the guards above it is not shown when syncing failed (which is not always an actual problem).
MaterialSyncMessage(self.device).show() 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 @staticmethod
def _getLocalMaterials() -> Dict[str, LocalMaterial]: 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] result = {} # type: Dict[str, LocalMaterial]
all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material") 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. 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.

View file

@ -12,9 +12,11 @@ from UM.Signal import Signal
from cura.CuraApplication import CuraApplication 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: 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. # The discovery protocol name for Ultimaker printers.
ZERO_CONF_NAME = u"_ultimaker._tcp.local." 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_event = None # type: Optional[Event]
self._service_changed_request_thread = None # type: Optional[Thread] 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: 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_queue = Queue()
self._service_changed_request_event = Event() self._service_changed_request_event = Event()
try: try:
@ -56,16 +61,18 @@ class ZeroConfClient:
self._zero_conf_browser.cancel() self._zero_conf_browser.cancel()
self._zero_conf_browser = None 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: 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) item = (zeroconf, service_type, name, state_change)
if not self._service_changed_request_queue or not self._service_changed_request_event: if not self._service_changed_request_queue or not self._service_changed_request_event:
return return
self._service_changed_request_queue.put(item) self._service_changed_request_queue.put(item)
self._service_changed_request_event.set() self._service_changed_request_event.set()
## Callback for when a ZeroConf service has changes.
def _handleOnServiceChangedRequests(self) -> None: 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: if not self._service_changed_request_queue or not self._service_changed_request_event:
return return
@ -98,19 +105,23 @@ class ZeroConfClient:
for request in reschedule_requests: for request in reschedule_requests:
self._service_changed_request_queue.put(request) self._service_changed_request_queue.put(request)
## Handler for zeroConf detection. def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str,
# Return True or False indicating if the process succeeded. state_change: ServiceStateChange) -> bool:
# Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. """Handler for zeroConf detection.
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange
) -> bool: 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: if state_change == ServiceStateChange.Added:
return self._onServiceAdded(zero_conf, service_type, name) return self._onServiceAdded(zero_conf, service_type, name)
elif state_change == ServiceStateChange.Removed: elif state_change == ServiceStateChange.Removed:
return self._onServiceRemoved(name) return self._onServiceRemoved(name)
return True return True
## Handler for when a ZeroConf service was added.
def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: 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 # First try getting info from zero-conf cache
info = ServiceInfo(service_type, name, properties={}) info = ServiceInfo(service_type, name, properties={})
for record in zero_conf.cache.entries_with_name(name.lower()): for record in zero_conf.cache.entries_with_name(name.lower()):
@ -141,8 +152,9 @@ class ZeroConfClient:
return True return True
## Handler for when a ZeroConf service was removed.
def _onServiceRemoved(self, name: str) -> bool: def _onServiceRemoved(self, name: str) -> bool:
"""Handler for when a ZeroConf service was removed."""
Logger.log("d", "ZeroConf service removed: %s" % name) Logger.log("d", "ZeroConf service removed: %s" % name)
self.removedNetworkCluster.emit(str(name)) self.removedNetworkCluster.emit(str(name))
return True return True

View file

@ -13,11 +13,11 @@ from .Network.LocalClusterOutputDeviceManager import LocalClusterOutputDeviceMan
from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager 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): class UM3OutputDevicePlugin(OutputDevicePlugin):
"""This plugin handles the discovery and networking for Ultimaker 3D printers"""
# Signal emitted when the list of discovered devices changed. Used by printer action in this plugin.
discoveredDevicesChanged = Signal() discoveredDevicesChanged = Signal()
"""Signal emitted when the list of discovered devices changed. Used by printer action in this plugin."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() 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. # This ensures no output devices are still connected that do not belong to the new active machine.
CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections)
## Start looking for devices in the network and cloud.
def start(self): def start(self):
"""Start looking for devices in the network and cloud."""
self._network_output_device_manager.start() self._network_output_device_manager.start()
self._cloud_output_device_manager.start() self._cloud_output_device_manager.start()
@ -43,31 +44,38 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._network_output_device_manager.stop() self._network_output_device_manager.stop()
self._cloud_output_device_manager.stop() self._cloud_output_device_manager.stop()
## Restart network discovery.
def startDiscovery(self) -> None: def startDiscovery(self) -> None:
"""Restart network discovery."""
self._network_output_device_manager.startDiscovery() self._network_output_device_manager.startDiscovery()
## Force refreshing the network connections.
def refreshConnections(self) -> None: def refreshConnections(self) -> None:
"""Force refreshing the network connections."""
self._network_output_device_manager.refreshConnections() self._network_output_device_manager.refreshConnections()
self._cloud_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: def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt:
"""Indicate that this plugin supports adding networked printers manually."""
return ManualDeviceAdditionAttempt.PRIORITY 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: 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) self._network_output_device_manager.addManualDevice(address, callback)
## Remove a manually connected networked printer.
def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: def removeManualDevice(self, key: str, address: Optional[str] = None) -> None:
"""Remove a manually connected networked printer."""
self._network_output_device_manager.removeManualDevice(key, address) self._network_output_device_manager.removeManualDevice(key, address)
## Get the discovered devices from the local network.
def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]:
"""Get the discovered devices from the local network."""
return self._network_output_device_manager.getDiscoveredDevices() return self._network_output_device_manager.getDiscoveredDevices()
## Connect the active machine to a device.
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
"""Connect the active machine to a device."""
self._network_output_device_manager.associateActiveMachineWithPrinterDevice(device) self._network_output_device_manager.associateActiveMachineWithPrinterDevice(device)

View file

@ -15,9 +15,11 @@ from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice
I18N_CATALOG = i18nCatalog("cura") 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): 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. # Signal emitted when discovered devices have changed.
discoveredDevicesChanged = pyqtSignal() discoveredDevicesChanged = pyqtSignal()
@ -27,59 +29,69 @@ class UltimakerNetworkedPrinterAction(MachineAction):
self._qml_url = "resources/qml/DiscoverUM3Action.qml" self._qml_url = "resources/qml/DiscoverUM3Action.qml"
self._network_plugin = None # type: Optional[UM3OutputDevicePlugin] self._network_plugin = None # type: Optional[UM3OutputDevicePlugin]
## Override the default value.
def needsUserInteraction(self) -> bool: def needsUserInteraction(self) -> bool:
"""Override the default value."""
return False return False
## Start listening to network discovery events via the plugin.
@pyqtSlot(name = "startDiscovery") @pyqtSlot(name = "startDiscovery")
def startDiscovery(self) -> None: def startDiscovery(self) -> None:
"""Start listening to network discovery events via the plugin."""
self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.discoveredDevicesChanged.emit() # trigger at least once to populate the list self.discoveredDevicesChanged.emit() # trigger at least once to populate the list
## Reset the discovered devices.
@pyqtSlot(name = "reset") @pyqtSlot(name = "reset")
def reset(self) -> None: def reset(self) -> None:
"""Reset the discovered devices."""
self.discoveredDevicesChanged.emit() # trigger to reset the list self.discoveredDevicesChanged.emit() # trigger to reset the list
## Reset the discovered devices.
@pyqtSlot(name = "restartDiscovery") @pyqtSlot(name = "restartDiscovery")
def restartDiscovery(self) -> None: def restartDiscovery(self) -> None:
"""Reset the discovered devices."""
self._networkPlugin.startDiscovery() self._networkPlugin.startDiscovery()
self.discoveredDevicesChanged.emit() # trigger to reset the list self.discoveredDevicesChanged.emit() # trigger to reset the list
## Remove a manually added device.
@pyqtSlot(str, str, name = "removeManualDevice") @pyqtSlot(str, str, name = "removeManualDevice")
def removeManualDevice(self, key: str, address: str) -> None: def removeManualDevice(self, key: str, address: str) -> None:
"""Remove a manually added device."""
self._networkPlugin.removeManualDevice(key, address) self._networkPlugin.removeManualDevice(key, address)
## Add a new manual device. Can replace an existing one by key.
@pyqtSlot(str, str, name = "setManualDevice") @pyqtSlot(str, str, name = "setManualDevice")
def setManualDevice(self, key: str, address: str) -> None: def setManualDevice(self, key: str, address: str) -> None:
"""Add a new manual device. Can replace an existing one by key."""
if key != "": if key != "":
self._networkPlugin.removeManualDevice(key) self._networkPlugin.removeManualDevice(key)
if address != "": if address != "":
self._networkPlugin.addManualDevice(address) self._networkPlugin.addManualDevice(address)
## Get the devices discovered in the local network sorted by name.
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged) @pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self): def foundDevices(self):
"""Get the devices discovered in the local network sorted by name."""
discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values()) discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values())
discovered_devices.sort(key = lambda d: d.name) discovered_devices.sort(key = lambda d: d.name)
return discovered_devices return discovered_devices
## Connect a device selected in the list with the active machine.
@pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice") @pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice")
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
"""Connect a device selected in the list with the active machine."""
self._networkPlugin.associateActiveMachineWithPrinterDevice(device) self._networkPlugin.associateActiveMachineWithPrinterDevice(device)
## Callback for when the list of discovered devices in the plugin was changed.
def _onDeviceDiscoveryChanged(self) -> None: def _onDeviceDiscoveryChanged(self) -> None:
"""Callback for when the list of discovered devices in the plugin was changed."""
self.discoveredDevicesChanged.emit() self.discoveredDevicesChanged.emit()
## Get the network manager from the plugin.
@property @property
def _networkPlugin(self) -> UM3OutputDevicePlugin: def _networkPlugin(self) -> UM3OutputDevicePlugin:
"""Get the network manager from the plugin."""
if not self._network_plugin: if not self._network_plugin:
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting") network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting")

View file

@ -22,10 +22,12 @@ from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus
from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus 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): 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_NETWORK_KEY = "um_network_key"
META_CLUSTER_ID = "um_cloud_cluster_id" META_CLUSTER_ID = "um_cloud_cluster_id"
@ -85,14 +87,16 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
# The job upload progress message modal. # The job upload progress message modal.
self._progress = PrintJobUploadProgressMessage() self._progress = PrintJobUploadProgressMessage()
## The IP address of the printer.
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def address(self) -> str: def address(self) -> str:
"""The IP address of the printer."""
return self._address return self._address
## The display name of the printer.
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def printerTypeName(self) -> str: def printerTypeName(self) -> str:
"""The display name of the printer."""
return self._printer_type_name return self._printer_type_name
# Get all print jobs for this cluster. # Get all print jobs for this cluster.
@ -157,13 +161,15 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
self._active_printer = printer self._active_printer = printer
self.activePrinterChanged.emit() self.activePrinterChanged.emit()
## Whether the printer that this output device represents supports print job actions via the local network.
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def supportsPrintJobActions(self) -> bool: def supportsPrintJobActions(self) -> bool:
"""Whether the printer that this output device represents supports print job actions via the local network."""
return True return True
## Set the remote print job state.
def setJobState(self, print_job_uuid: str, state: str) -> None: def setJobState(self, print_job_uuid: str, state: str) -> None:
"""Set the remote print job state."""
raise NotImplementedError("setJobState must be implemented") raise NotImplementedError("setJobState must be implemented")
@pyqtSlot(str, name="sendJobToTop") @pyqtSlot(str, name="sendJobToTop")
@ -210,11 +216,13 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
self._checkStillConnected() self._checkStillConnected()
super()._update() 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: 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 time_since_last_response = time() - self._time_of_last_response
if time_since_last_response > self.NETWORK_RESPONSE_CONSIDER_OFFLINE: if time_since_last_response > self.NETWORK_RESPONSE_CONSIDER_OFFLINE:
self.setConnectionState(ConnectionState.Closed) self.setConnectionState(ConnectionState.Closed)
@ -223,9 +231,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
elif self.connectionState == ConnectionState.Closed: elif self.connectionState == ConnectionState.Closed:
self._reconnectForActiveMachine() self._reconnectForActiveMachine()
## Reconnect for the active output device.
# Does nothing if the device is not meant for the active machine.
def _reconnectForActiveMachine(self) -> None: 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() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine: if not active_machine:
return return
@ -281,16 +291,19 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
self.printersChanged.emit() self.printersChanged.emit()
self._checkIfClusterHost() self._checkIfClusterHost()
## Check is this device is a cluster host and takes the needed actions when it is not.
def _checkIfClusterHost(self): 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(): if len(self._printers) < 1 and self.isConnected():
NotClusterHostMessage(self).show() NotClusterHostMessage(self).show()
self.close() self.close()
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(self.key) 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: 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() self._responseReceived()
# Keep track of the new print jobs to show. # Keep track of the new print jobs to show.
@ -321,9 +334,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
self._print_jobs = new_print_jobs self._print_jobs = new_print_jobs
self.printJobsChanged.emit() 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: 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)) model = remote_job.createOutputModel(ClusterOutputController(self))
if remote_job.printer_uuid: if remote_job.printer_uuid:
self._updateAssignedPrinter(model, remote_job.printer_uuid) self._updateAssignedPrinter(model, remote_job.printer_uuid)
@ -333,16 +348,18 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
model.loadPreviewImageFromUrl(remote_job.preview_url) model.loadPreviewImageFromUrl(remote_job.preview_url)
return model return model
## Updates the printer assignment for the given print job model.
def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: 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) printer = next((p for p in self._printers if printer_uuid == p.key), None)
if not printer: if not printer:
return return
printer.updateActivePrintJob(model) printer.updateActivePrintJob(model)
model.updateAssignedPrinter(printer) model.updateAssignedPrinter(printer)
## Load Monitor tab QML.
def _loadMonitorTab(self) -> None: def _loadMonitorTab(self) -> None:
"""Load Monitor tab QML."""
plugin_registry = CuraApplication.getInstance().getPluginRegistry() plugin_registry = CuraApplication.getInstance().getPluginRegistry()
if not plugin_registry: if not plugin_registry:
Logger.log("e", "Could not get plugin registry") Logger.log("e", "Could not get plugin registry")