mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-08 07:27:29 -06:00
Convert doxygen to rst for UM3NetworkPrinting
This commit is contained in:
parent
de82406782
commit
5eb5ffd916
38 changed files with 797 additions and 487 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -7,11 +7,11 @@ 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__(
|
||||||
|
|
|
@ -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__(
|
||||||
|
|
|
@ -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__(
|
||||||
|
|
|
@ -7,8 +7,8 @@ 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__(
|
||||||
|
|
|
@ -7,8 +7,8 @@ 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__(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -7,8 +7,8 @@ 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__(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)\
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue