mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-20 13:17:51 -06:00
Merge pull request #13165 from Ultimaker/CURA-8463_cloud_configuration
Show all configurations in "abstract" printer from cloud
This commit is contained in:
commit
a713b1e9e9
16 changed files with 400 additions and 193 deletions
|
@ -99,6 +99,7 @@ class MachineListModel(ListModel):
|
||||||
if self._show_cloud_printers:
|
if self._show_cloud_printers:
|
||||||
self.addItem(stack)
|
self.addItem(stack)
|
||||||
# Remove this machine from the other stack list
|
# Remove this machine from the other stack list
|
||||||
|
if stack in other_machine_stacks:
|
||||||
other_machine_stacks.remove(stack)
|
other_machine_stacks.remove(stack)
|
||||||
|
|
||||||
if len(abstract_machine_stacks) > 0:
|
if len(abstract_machine_stacks) > 0:
|
||||||
|
|
|
@ -56,7 +56,6 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
For all other uses it should be used in the same way as a "regular" OutputDevice.
|
For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
printersChanged = pyqtSignal()
|
printersChanged = pyqtSignal()
|
||||||
connectionStateChanged = pyqtSignal(str)
|
connectionStateChanged = pyqtSignal(str)
|
||||||
acceptsCommandsChanged = pyqtSignal()
|
acceptsCommandsChanged = pyqtSignal()
|
||||||
|
@ -183,8 +182,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
@pyqtProperty(QObject, constant = True)
|
@pyqtProperty(QObject, constant = True)
|
||||||
def monitorItem(self) -> QObject:
|
def monitorItem(self) -> QObject:
|
||||||
# Note that we specifically only check if the monitor component is created.
|
# Note that we specifically only check if the monitor component is created.
|
||||||
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try
|
||||||
# create the item (and fail) every time.
|
# to create the item (and fail) every time.
|
||||||
if not self._monitor_component:
|
if not self._monitor_component:
|
||||||
self._createMonitorViewFromQML()
|
self._createMonitorViewFromQML()
|
||||||
return self._monitor_item
|
return self._monitor_item
|
||||||
|
@ -237,9 +236,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
self.acceptsCommandsChanged.emit()
|
self.acceptsCommandsChanged.emit()
|
||||||
|
|
||||||
# Returns the unique configurations of the printers within this output device
|
|
||||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||||
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
||||||
|
""" Returns the unique configurations of the printers within this output device """
|
||||||
return self._unique_configurations
|
return self._unique_configurations
|
||||||
|
|
||||||
def _updateUniqueConfigurations(self) -> None:
|
def _updateUniqueConfigurations(self) -> None:
|
||||||
|
@ -248,7 +247,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
|
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
|
||||||
all_configurations.add(printer.printerConfiguration)
|
all_configurations.add(printer.printerConfiguration)
|
||||||
all_configurations.update(printer.availableConfigurations)
|
all_configurations.update(printer.availableConfigurations)
|
||||||
if None in all_configurations: # Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration. List could end up empty!
|
if None in all_configurations:
|
||||||
|
# Shouldn't happen, but it does. I don't see how it could ever happen. Skip adding that configuration.
|
||||||
|
# List could end up empty!
|
||||||
Logger.log("e", "Found a broken configuration in the synced list!")
|
Logger.log("e", "Found a broken configuration in the synced list!")
|
||||||
all_configurations.remove(None)
|
all_configurations.remove(None)
|
||||||
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
|
new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "")
|
||||||
|
@ -256,9 +257,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self._unique_configurations = new_configurations
|
self._unique_configurations = new_configurations
|
||||||
self.uniqueConfigurationsChanged.emit()
|
self.uniqueConfigurationsChanged.emit()
|
||||||
|
|
||||||
# Returns the unique configurations of the printers within this output device
|
|
||||||
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
|
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
|
||||||
def uniquePrinterTypes(self) -> List[str]:
|
def uniquePrinterTypes(self) -> List[str]:
|
||||||
|
""" Returns the unique configurations of the printers within this output device """
|
||||||
return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
|
return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations])))
|
||||||
|
|
||||||
def _onPrintersChanged(self) -> None:
|
def _onPrintersChanged(self) -> None:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, cast
|
||||||
|
|
||||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
@ -275,41 +275,26 @@ class CuraStackBuilder:
|
||||||
:return: The new Abstract Machine or None if an error occurred.
|
:return: The new Abstract Machine or None if an error occurred.
|
||||||
"""
|
"""
|
||||||
abstract_machine_id = f"{definition_id}_abstract_machine"
|
abstract_machine_id = f"{definition_id}_abstract_machine"
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
application = CuraApplication.getInstance()
|
application = CuraApplication.getInstance()
|
||||||
registry = application.getContainerRegistry()
|
registry = application.getContainerRegistry()
|
||||||
container_tree = ContainerTree.getInstance()
|
|
||||||
|
|
||||||
if registry.findContainerStacks(is_abstract_machine = "True", id = abstract_machine_id):
|
abstract_machines = registry.findContainerStacks(id = abstract_machine_id)
|
||||||
# This abstract machine already exists
|
if abstract_machines:
|
||||||
|
return cast(GlobalStack, abstract_machines[0])
|
||||||
|
definitions = registry.findDefinitionContainers(id=definition_id)
|
||||||
|
|
||||||
|
name = ""
|
||||||
|
|
||||||
|
if definitions:
|
||||||
|
name = definitions[0].getName()
|
||||||
|
stack = cls.createMachine(abstract_machine_id, definition_id)
|
||||||
|
if not stack:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
match registry.findDefinitionContainers(type = "machine", id = definition_id):
|
|
||||||
case []:
|
|
||||||
# It should not be possible for the definition to be missing since an abstract machine will only
|
|
||||||
# be created as a result of a machine with definition_id being created.
|
|
||||||
Logger.error(f"Definition {definition_id} was not found!")
|
|
||||||
return None
|
|
||||||
case [machine_definition, *_definitions]:
|
|
||||||
machine_node = container_tree.machines[machine_definition.getId()]
|
|
||||||
name = machine_definition.getName()
|
|
||||||
|
|
||||||
stack = GlobalStack(abstract_machine_id)
|
|
||||||
stack.setMetaDataEntry("is_abstract_machine", True)
|
|
||||||
stack.setMetaDataEntry("is_online", True)
|
|
||||||
stack.setDefinition(machine_definition)
|
|
||||||
cls.createUserContainer(
|
|
||||||
name,
|
|
||||||
machine_definition,
|
|
||||||
stack,
|
|
||||||
application.empty_variant_container,
|
|
||||||
application.empty_material_container,
|
|
||||||
machine_node.preferredGlobalQuality().container,
|
|
||||||
)
|
|
||||||
|
|
||||||
stack.setName(name)
|
stack.setName(name)
|
||||||
|
|
||||||
registry.addContainer(stack)
|
stack.setMetaDataEntry("is_abstract_machine", True)
|
||||||
|
stack.setMetaDataEntry("is_online", True)
|
||||||
|
|
||||||
return stack
|
return stack
|
|
@ -292,7 +292,6 @@ class GlobalStack(CuraContainerStack):
|
||||||
for extruder_train in extruder_trains:
|
for extruder_train in extruder_trains:
|
||||||
extruder_position = extruder_train.getMetaDataEntry("position")
|
extruder_position = extruder_train.getMetaDataEntry("position")
|
||||||
extruder_check_position.add(extruder_position)
|
extruder_check_position.add(extruder_position)
|
||||||
|
|
||||||
for check_position in range(machine_extruder_count):
|
for check_position in range(machine_extruder_count):
|
||||||
if str(check_position) not in extruder_check_position:
|
if str(check_position) not in extruder_check_position:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -359,6 +359,7 @@ class MachineManager(QObject):
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
extruder_manager.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
if not global_stack.isValid():
|
if not global_stack.isValid():
|
||||||
|
Logger.warning("Global stack isn't valid, adding it to faulty container list")
|
||||||
# Mark global stack as invalid
|
# Mark global stack as invalid
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
||||||
return # We're done here
|
return # We're done here
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
from time import time
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QObject
|
||||||
|
from PyQt6.QtNetwork import QNetworkReply
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
|
||||||
|
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
||||||
|
from .CloudApiClient import CloudApiClient
|
||||||
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
|
from ..Models.Http.CloudClusterWithConfigResponse import CloudClusterWithConfigResponse
|
||||||
|
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
|
||||||
|
|
||||||
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractCloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
|
API_CHECK_INTERVAL = 10.0 # seconds
|
||||||
|
|
||||||
|
def __init__(self, api_client: CloudApiClient, printer_type: str, parent: QObject = None) -> None:
|
||||||
|
|
||||||
|
self._api = api_client
|
||||||
|
properties = {b"printer_type": printer_type.encode()}
|
||||||
|
super().__init__(
|
||||||
|
device_id=f"ABSTRACT_{printer_type}",
|
||||||
|
address="",
|
||||||
|
connection_type=ConnectionType.CloudConnection,
|
||||||
|
properties=properties,
|
||||||
|
parent=parent
|
||||||
|
)
|
||||||
|
|
||||||
|
self._setInterfaceElements()
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""Connects this device."""
|
||||||
|
|
||||||
|
if self.isConnected():
|
||||||
|
return
|
||||||
|
Logger.log("i", "Attempting to connect AbstractCloudOutputDevice %s", self.key)
|
||||||
|
super().connect()
|
||||||
|
|
||||||
|
#CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Disconnects the device"""
|
||||||
|
|
||||||
|
if not self.isConnected():
|
||||||
|
return
|
||||||
|
super().disconnect()
|
||||||
|
|
||||||
|
def _update(self) -> None:
|
||||||
|
"""Called when the network data should be updated."""
|
||||||
|
|
||||||
|
super()._update()
|
||||||
|
if time() - self._time_of_last_request < self.API_CHECK_INTERVAL:
|
||||||
|
return # avoid calling the cloud too often
|
||||||
|
self._time_of_last_request = time()
|
||||||
|
if self._api.account.isLoggedIn:
|
||||||
|
self.setAuthenticationState(AuthState.Authenticated)
|
||||||
|
self._last_request_time = time()
|
||||||
|
self._api.getClustersByMachineType(self.printerType, self._onCompleted, self._onError)
|
||||||
|
else:
|
||||||
|
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||||
|
|
||||||
|
def _setInterfaceElements(self) -> None:
|
||||||
|
"""Set all the interface elements and texts for this output device."""
|
||||||
|
|
||||||
|
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'.
|
||||||
|
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via cloud"))
|
||||||
|
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via cloud"))
|
||||||
|
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via cloud"))
|
||||||
|
|
||||||
|
def _onCompleted(self, clusters: List[CloudClusterWithConfigResponse]) -> None:
|
||||||
|
self._responseReceived()
|
||||||
|
|
||||||
|
all_configurations = []
|
||||||
|
for resp in clusters:
|
||||||
|
if resp.configuration is not None:
|
||||||
|
# Usually when the printer is offline, it doesn't have a configuration...
|
||||||
|
all_configurations.append(resp.configuration)
|
||||||
|
self._updatePrinters(all_configurations)
|
||||||
|
|
||||||
|
def _onError(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||||
|
pass
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import json
|
import json
|
||||||
|
import urllib.parse
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
|
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
|
||||||
|
@ -17,6 +18,7 @@ from cura.UltimakerCloud import UltimakerCloudConstants
|
||||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||||
from .ToolPathUploader import ToolPathUploader
|
from .ToolPathUploader import ToolPathUploader
|
||||||
from ..Models.BaseModel import BaseModel
|
from ..Models.BaseModel import BaseModel
|
||||||
|
from ..Models.Http.CloudClusterWithConfigResponse import CloudClusterWithConfigResponse
|
||||||
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
from ..Models.Http.CloudClusterStatus import CloudClusterStatus
|
from ..Models.Http.CloudClusterStatus import CloudClusterStatus
|
||||||
from ..Models.Http.CloudError import CloudError
|
from ..Models.Http.CloudError import CloudError
|
||||||
|
@ -48,7 +50,6 @@ class CloudApiClient:
|
||||||
"""Initializes a new cloud API client.
|
"""Initializes a new cloud API client.
|
||||||
|
|
||||||
:param app:
|
:param app:
|
||||||
:param account: The user's account object
|
|
||||||
:param on_error: The callback to be called whenever we receive errors from the server.
|
:param on_error: The callback to be called whenever we receive errors from the server.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -57,12 +58,11 @@ class CloudApiClient:
|
||||||
self._scope = JsonDecoratorScope(UltimakerCloudScope(app))
|
self._scope = JsonDecoratorScope(UltimakerCloudScope(app))
|
||||||
self._http = HttpRequestManager.getInstance()
|
self._http = HttpRequestManager.getInstance()
|
||||||
self._on_error = on_error
|
self._on_error = on_error
|
||||||
self._upload = None # type: Optional[ToolPathUploader]
|
self._upload: Optional[ToolPathUploader] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account(self) -> Account:
|
def account(self) -> Account:
|
||||||
"""Gets the account used for the API."""
|
"""Gets the account used for the API."""
|
||||||
|
|
||||||
return self._account
|
return self._account
|
||||||
|
|
||||||
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
|
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None:
|
||||||
|
@ -71,13 +71,31 @@ class CloudApiClient:
|
||||||
:param on_finished: The function to be called after the result is parsed.
|
:param on_finished: The function to be called after the result is parsed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
|
url = f"{self.CLUSTER_API_ROOT}/clusters?status=active"
|
||||||
self._http.get(url,
|
self._http.get(url,
|
||||||
scope = self._scope,
|
scope = self._scope,
|
||||||
callback = self._parseCallback(on_finished, CloudClusterResponse, failed),
|
callback = self._parseCallback(on_finished, CloudClusterResponse, failed),
|
||||||
error_callback = failed,
|
error_callback = failed,
|
||||||
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
timeout = self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
|
def getClustersByMachineType(self, machine_type, on_finished: Callable[[List[CloudClusterWithConfigResponse]], Any], failed: Callable) -> None:
|
||||||
|
# HACK: There is something weird going on with the API, as it reports printer types in formats like
|
||||||
|
# "ultimaker_s3", but wants "Ultimaker S3" when using the machine_variant filter query. So we need to do some
|
||||||
|
# conversion!
|
||||||
|
|
||||||
|
machine_type = machine_type.replace("_plus", "+")
|
||||||
|
machine_type = machine_type.replace("_", " ")
|
||||||
|
machine_type = machine_type.replace("ultimaker", "ultimaker ")
|
||||||
|
machine_type = machine_type.replace(" ", " ")
|
||||||
|
machine_type = machine_type.title()
|
||||||
|
machine_type = urllib.parse.quote_plus(machine_type)
|
||||||
|
url = f"{self.CLUSTER_API_ROOT}/clusters?machine_variant={machine_type}"
|
||||||
|
self._http.get(url,
|
||||||
|
scope=self._scope,
|
||||||
|
callback=self._parseCallback(on_finished, CloudClusterWithConfigResponse, failed),
|
||||||
|
error_callback=failed,
|
||||||
|
timeout=self.DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
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.
|
"""Retrieves the status of the given cluster.
|
||||||
|
|
||||||
|
@ -85,7 +103,7 @@ class CloudApiClient:
|
||||||
:param on_finished: The function to be called after the result is parsed.
|
:param on_finished: The function to be called after the result is parsed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
|
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/status"
|
||||||
self._http.get(url,
|
self._http.get(url,
|
||||||
scope = self._scope,
|
scope = self._scope,
|
||||||
callback = self._parseCallback(on_finished, CloudClusterStatus),
|
callback = self._parseCallback(on_finished, CloudClusterStatus),
|
||||||
|
@ -100,7 +118,7 @@ class CloudApiClient:
|
||||||
:param on_finished: The function to be called after the result is parsed.
|
:param on_finished: The function to be called after the result is parsed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
url = f"{self.CURA_API_ROOT}/jobs/upload"
|
||||||
data = json.dumps({"data": request.toDict()}).encode()
|
data = json.dumps({"data": request.toDict()}).encode()
|
||||||
|
|
||||||
self._http.put(url,
|
self._http.put(url,
|
||||||
|
@ -131,7 +149,7 @@ class CloudApiClient:
|
||||||
# specific to sending print jobs) such as lost connection, unparsable responses, etc. are not returned here, but
|
# specific to sending print jobs) such as lost connection, unparsable responses, etc. are not returned here, but
|
||||||
# handled in a generic way by the CloudApiClient.
|
# handled in a generic way by the CloudApiClient.
|
||||||
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None:
|
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None:
|
||||||
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
|
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print/{job_id}"
|
||||||
self._http.post(url,
|
self._http.post(url,
|
||||||
scope = self._scope,
|
scope = self._scope,
|
||||||
data = b"",
|
data = b"",
|
||||||
|
@ -150,7 +168,7 @@ class CloudApiClient:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print_jobs/{cluster_job_id}/action/{action}"
|
||||||
self._http.post(url,
|
self._http.post(url,
|
||||||
scope = self._scope,
|
scope = self._scope,
|
||||||
data = body,
|
data = body,
|
||||||
|
@ -159,7 +177,7 @@ class CloudApiClient:
|
||||||
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.
|
"""We override _createEmptyRequest in order to add the user credentials.
|
||||||
|
|
||||||
:param url: The URL to request
|
:param path: The URL to request
|
||||||
:param content_type: The type of the body contents.
|
:param content_type: The type of the body contents.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -168,7 +186,7 @@ class CloudApiClient:
|
||||||
request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, content_type)
|
request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, content_type)
|
||||||
access_token = self._account.accessToken
|
access_token = self._account.accessToken
|
||||||
if access_token:
|
if access_token:
|
||||||
request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode())
|
request.setRawHeader(b"Authorization", f"Bearer {access_token}".encode())
|
||||||
return request
|
return request
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -42,7 +42,7 @@ I18N_CATALOG = i18nCatalog("cura")
|
||||||
class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
"""The cloud output device is a network output device that works remotely but has limited functionality.
|
"""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.
|
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.
|
As such, those methods have been implemented here.
|
||||||
Note that this device represents a single remote cluster, not a list of multiple clusters.
|
Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||||
"""
|
"""
|
||||||
|
@ -59,7 +59,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.2.12")
|
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.2.12")
|
||||||
|
|
||||||
# Notify can only use signals that are defined by the class that they are in, not inherited ones.
|
# Notify can only use signals that are defined by the class that they are in, not inherited ones.
|
||||||
# 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()
|
||||||
|
|
||||||
def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
|
def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
|
||||||
|
@ -203,7 +203,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
# Note that self.writeFinished is called in _onPrintUploadCompleted as well.
|
# Note that self.writeFinished is called in _onPrintUploadCompleted as well.
|
||||||
if self._uploaded_print_job:
|
if self._uploaded_print_job:
|
||||||
Logger.log("i", "Current mesh is already attached to a print-job, immediately request reprint.")
|
Logger.log("i", "Current mesh is already attached to a print-job, immediately request reprint.")
|
||||||
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted, self._onPrintUploadSpecificError)
|
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted,
|
||||||
|
self._onPrintUploadSpecificError)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Export the scene to the correct file type.
|
# Export the scene to the correct file type.
|
||||||
|
@ -246,12 +247,15 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
self._progress.update(100)
|
self._progress.update(100)
|
||||||
print_job = cast(CloudPrintJobResponse, self._pre_upload_print_job)
|
print_job = cast(CloudPrintJobResponse, self._pre_upload_print_job)
|
||||||
if not print_job: # It's possible that another print job is requested in the meanwhile, which then fails to upload with an error, which sets self._pre_uploaded_print_job to `None`.
|
if not print_job:
|
||||||
|
# It's possible that another print job is requested in the meanwhile, which then fails to upload with an
|
||||||
|
# error, which sets self._pre_uploaded_print_job to `None`.
|
||||||
self._pre_upload_print_job = None
|
self._pre_upload_print_job = None
|
||||||
self._uploaded_print_job = None
|
self._uploaded_print_job = None
|
||||||
Logger.log("w", "Interference from another job uploaded at roughly the same time, not uploading print!")
|
Logger.log("w", "Interference from another job uploaded at roughly the same time, not uploading print!")
|
||||||
return # Prevent a crash.
|
return # Prevent a crash.
|
||||||
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted, self._onPrintUploadSpecificError)
|
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted,
|
||||||
|
self._onPrintUploadSpecificError)
|
||||||
|
|
||||||
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
||||||
"""Shows a message when the upload has succeeded
|
"""Shows a message when the upload has succeeded
|
||||||
|
@ -285,7 +289,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
if error_code == 409:
|
if error_code == 409:
|
||||||
PrintJobUploadQueueFullMessage().show()
|
PrintJobUploadQueueFullMessage().show()
|
||||||
else:
|
else:
|
||||||
PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send", "Unknown error code when uploading print job: {0}", error_code)).show()
|
PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send",
|
||||||
|
"Unknown error code when uploading print job: {0}",
|
||||||
|
error_code)).show()
|
||||||
|
|
||||||
Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code))
|
Logger.log("w", "Upload of print job failed specifically with error code {}".format(error_code))
|
||||||
|
|
||||||
|
@ -343,11 +349,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
@pyqtSlot(name="openPrintJobControlPanel")
|
@pyqtSlot(name="openPrintJobControlPanel")
|
||||||
def openPrintJobControlPanel(self) -> None:
|
def openPrintJobControlPanel(self) -> None:
|
||||||
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl + "?utm_source=cura&utm_medium=software&utm_campaign=monitor-manage-browser"))
|
QDesktopServices.openUrl(QUrl(f"{self.clusterCloudUrl}?utm_source=cura&utm_medium=software&"
|
||||||
|
f"utm_campaign=monitor-manage-browser"))
|
||||||
|
|
||||||
@pyqtSlot(name="openPrinterControlPanel")
|
@pyqtSlot(name="openPrinterControlPanel")
|
||||||
def openPrinterControlPanel(self) -> None:
|
def openPrinterControlPanel(self) -> None:
|
||||||
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl + "?utm_source=cura&utm_medium=software&utm_campaign=monitor-manage-printer"))
|
QDesktopServices.openUrl(QUrl(f"{self.clusterCloudUrl}?utm_source=cura&utm_medium=software"
|
||||||
|
f"&utm_campaign=monitor-manage-printer"))
|
||||||
|
|
||||||
permissionsChanged = pyqtSignal()
|
permissionsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@ -369,7 +377,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
@pyqtProperty(bool, notify = permissionsChanged)
|
@pyqtProperty(bool, notify = permissionsChanged)
|
||||||
def canWriteOwnPrintJobs(self) -> bool:
|
def canWriteOwnPrintJobs(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Whether this user can change things about print jobs made by themself.
|
Whether this user can change things about print jobs made by them.
|
||||||
"""
|
"""
|
||||||
return "digital-factory.print-job.write.own" in self._account.permissions
|
return "digital-factory.print-job.write.own" in self._account.permissions
|
||||||
|
|
||||||
|
@ -397,4 +405,4 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
"""Gets the URL on which to monitor the cluster via the cloud."""
|
"""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://digitalfactory{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id)
|
return f"https://digitalfactory{root_url_prefix}.ultimaker.com/app/jobs/{self.clusterData.cluster_id}"
|
||||||
|
|
|
@ -9,7 +9,6 @@ from PyQt6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM import i18nCatalog
|
from UM import i18nCatalog
|
||||||
from UM.Logger import Logger # To log errors talking to the API.
|
from UM.Logger import Logger # To log errors talking to the API.
|
||||||
from UM.Message import Message
|
|
||||||
from UM.Settings.Interfaces import ContainerInterface
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
@ -20,16 +19,19 @@ from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To upda
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES, META_UM_LINKED_TO_ACCOUNT
|
from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES, META_UM_LINKED_TO_ACCOUNT
|
||||||
|
from .AbstractCloudOutputDevice import AbstractCloudOutputDevice
|
||||||
from .CloudApiClient import CloudApiClient
|
from .CloudApiClient import CloudApiClient
|
||||||
from .CloudOutputDevice import CloudOutputDevice
|
from .CloudOutputDevice import CloudOutputDevice
|
||||||
|
from ..Messages.RemovedPrintersMessage import RemovedPrintersMessage
|
||||||
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
|
from ..Messages.NewPrinterDetectedMessage import NewPrinterDetectedMessage
|
||||||
|
|
||||||
|
|
||||||
class CloudOutputDeviceManager:
|
class CloudOutputDeviceManager:
|
||||||
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
||||||
|
|
||||||
Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
||||||
API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
API spec is available on https://docs.api.ultimaker.com/connect/index.html.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
META_CLUSTER_ID = "um_cloud_cluster_id"
|
META_CLUSTER_ID = "um_cloud_cluster_id"
|
||||||
|
@ -46,21 +48,22 @@ class CloudOutputDeviceManager:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# Persistent dict containing the remote clusters for the authenticated user.
|
# Persistent dict containing the remote clusters for the authenticated user.
|
||||||
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
|
self._remote_clusters: Dict[str, CloudOutputDevice] = {}
|
||||||
|
|
||||||
|
self._abstract_clusters: Dict[str, AbstractCloudOutputDevice] = {}
|
||||||
|
|
||||||
# Dictionary containing all the cloud printers loaded in Cura
|
# Dictionary containing all the cloud printers loaded in Cura
|
||||||
self._um_cloud_printers = {} # type: Dict[str, GlobalStack]
|
self._um_cloud_printers: Dict[str, GlobalStack] = {}
|
||||||
|
|
||||||
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
self._account: Account = CuraApplication.getInstance().getCuraAPI().account
|
||||||
self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
|
self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
|
||||||
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||||
self._removed_printers_message = None # type: Optional[Message]
|
self._removed_printers_message: Optional[RemovedPrintersMessage] = None
|
||||||
|
|
||||||
# Ensure we don't start twice.
|
# Ensure we don't start twice.
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
self._syncing = False
|
self._syncing = False
|
||||||
|
|
||||||
CuraApplication.getInstance().getContainerRegistry().containerRemoved.connect(self._printerRemoved)
|
CuraApplication.getInstance().getContainerRegistry().containerRemoved.connect(self._printerRemoved)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
@ -113,8 +116,8 @@ class CloudOutputDeviceManager:
|
||||||
CuraApplication.getInstance().getContainerRegistry().findContainerStacks(
|
CuraApplication.getInstance().getContainerRegistry().findContainerStacks(
|
||||||
type = "machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None)}
|
type = "machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None)}
|
||||||
new_clusters = []
|
new_clusters = []
|
||||||
all_clusters = {c.cluster_id: c for c in clusters} # type: Dict[str, CloudClusterResponse]
|
all_clusters: Dict[str, CloudClusterResponse] = {c.cluster_id: c for c in clusters}
|
||||||
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
|
online_clusters: Dict[str, CloudClusterResponse] = {c.cluster_id: c for c in clusters if c.is_online}
|
||||||
|
|
||||||
# Add the new printers in Cura.
|
# Add the new printers in Cura.
|
||||||
for device_id, cluster_data in all_clusters.items():
|
for device_id, cluster_data in all_clusters.items():
|
||||||
|
@ -130,8 +133,11 @@ class CloudOutputDeviceManager:
|
||||||
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
||||||
if not self._um_cloud_printers[device_id].getMetaDataEntry(META_CAPABILITIES, None):
|
if not self._um_cloud_printers[device_id].getMetaDataEntry(META_CAPABILITIES, None):
|
||||||
self._um_cloud_printers[device_id].setMetaDataEntry(META_CAPABILITIES, ",".join(cluster_data.capabilities))
|
self._um_cloud_printers[device_id].setMetaDataEntry(META_CAPABILITIES, ",".join(cluster_data.capabilities))
|
||||||
self._onDevicesDiscovered(new_clusters)
|
|
||||||
|
|
||||||
|
# We want a machine stack per remote printer that we discovered. Create them now!
|
||||||
|
self._createMachineStacksForDiscoveredClusters(new_clusters)
|
||||||
|
|
||||||
|
# Update the online vs offline status for all found devices
|
||||||
self._updateOnlinePrinters(all_clusters)
|
self._updateOnlinePrinters(all_clusters)
|
||||||
|
|
||||||
# Hide the current removed_printers_message, if there is any
|
# Hide the current removed_printers_message, if there is any
|
||||||
|
@ -152,6 +158,7 @@ class CloudOutputDeviceManager:
|
||||||
|
|
||||||
if new_clusters or offline_device_keys or removed_device_keys:
|
if new_clusters or offline_device_keys or removed_device_keys:
|
||||||
self.discoveredDevicesChanged.emit()
|
self.discoveredDevicesChanged.emit()
|
||||||
|
|
||||||
if offline_device_keys:
|
if offline_device_keys:
|
||||||
# If the removed device was active we should connect to the new active device
|
# If the removed device was active we should connect to the new active device
|
||||||
self._connectToActiveMachine()
|
self._connectToActiveMachine()
|
||||||
|
@ -165,54 +172,62 @@ class CloudOutputDeviceManager:
|
||||||
self._syncing = False
|
self._syncing = False
|
||||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||||
|
|
||||||
def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None:
|
def _createMachineStacksForDiscoveredClusters(self, discovered_clusters: List[CloudClusterResponse]) -> None:
|
||||||
"""**Synchronously** create machines for discovered devices
|
"""**Synchronously** create machines for discovered devices
|
||||||
|
|
||||||
Any new machines are made available to the user.
|
Any new machines are made available to the user.
|
||||||
May take a long time to complete. As this code needs access to the Application
|
May take a long time to complete. This currently forcefully calls the "processEvents", which isn't
|
||||||
and blocks the GIL, creating a Job for this would not make sense.
|
the nicest solution out there. We might need to consider moving this into a job later!
|
||||||
Shows a Message informing the user of progress.
|
|
||||||
"""
|
"""
|
||||||
new_devices = []
|
new_output_devices: List[CloudOutputDevice] = []
|
||||||
remote_clusters_added = False
|
remote_clusters_added = False
|
||||||
host_guid_map = {machine.getMetaDataEntry(self.META_HOST_GUID): device_cluster_id
|
|
||||||
|
# Create a map that maps the HOST_GUID to the DEVICE_CLUSTER_ID
|
||||||
|
host_guid_map: Dict[str, str] = {machine.getMetaDataEntry(self.META_HOST_GUID): device_cluster_id
|
||||||
for device_cluster_id, machine in self._um_cloud_printers.items()
|
for device_cluster_id, machine in self._um_cloud_printers.items()
|
||||||
if machine.getMetaDataEntry(self.META_HOST_GUID)}
|
if machine.getMetaDataEntry(self.META_HOST_GUID)}
|
||||||
|
|
||||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
|
||||||
for cluster_data in clusters:
|
for cluster_data in discovered_clusters:
|
||||||
device = CloudOutputDevice(self._api, cluster_data)
|
output_device = CloudOutputDevice(self._api, cluster_data)
|
||||||
|
|
||||||
|
if cluster_data.printer_type not in self._abstract_clusters:
|
||||||
|
self._abstract_clusters[cluster_data.printer_type] = AbstractCloudOutputDevice(self._api, cluster_data.printer_type)
|
||||||
|
|
||||||
# If the machine already existed before, it will be present in the host_guid_map
|
# If the machine already existed before, it will be present in the host_guid_map
|
||||||
if cluster_data.host_guid in host_guid_map:
|
if cluster_data.host_guid in host_guid_map:
|
||||||
machine = machine_manager.getMachine(device.printerType, {self.META_HOST_GUID: cluster_data.host_guid})
|
machine = machine_manager.getMachine(output_device.printerType, {self.META_HOST_GUID: cluster_data.host_guid})
|
||||||
if machine and machine.getMetaDataEntry(self.META_CLUSTER_ID) != device.key:
|
if machine and machine.getMetaDataEntry(self.META_CLUSTER_ID) != output_device.key:
|
||||||
# If the retrieved device has a different cluster_id than the existing machine, bring the existing
|
# If the retrieved device has a different cluster_id than the existing machine, bring the existing
|
||||||
# machine up-to-date.
|
# machine up-to-date.
|
||||||
self._updateOutdatedMachine(outdated_machine = machine, new_cloud_output_device = device)
|
self._updateOutdatedMachine(outdated_machine = machine, new_cloud_output_device = output_device)
|
||||||
|
|
||||||
# Create a machine if we don't already have it. Do not make it the active machine.
|
# Create a machine if we don't already have it. Do not make it the active machine.
|
||||||
# We only need to add it if it wasn't already added by "local" network or by cloud.
|
# We only need to add it if it wasn't already added by "local" network or by cloud.
|
||||||
if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \
|
if machine_manager.getMachine(output_device.printerType, {self.META_CLUSTER_ID: output_device.key}) is None \
|
||||||
and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key.
|
and machine_manager.getMachine(output_device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key.
|
||||||
new_devices.append(device)
|
new_output_devices.append(output_device)
|
||||||
elif device.getId() not in self._remote_clusters:
|
elif output_device.getId() not in self._remote_clusters:
|
||||||
self._remote_clusters[device.getId()] = device
|
self._remote_clusters[output_device.getId()] = output_device
|
||||||
remote_clusters_added = True
|
remote_clusters_added = True
|
||||||
# If a printer that was removed from the account is re-added, change its metadata to mark it not removed
|
# If a printer that was removed from the account is re-added, change its metadata to mark it not removed
|
||||||
# from the account
|
# from the account
|
||||||
elif not parseBool(self._um_cloud_printers[device.key].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
|
elif not parseBool(self._um_cloud_printers[output_device.key].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
|
||||||
self._um_cloud_printers[device.key].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
self._um_cloud_printers[output_device.key].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
||||||
|
# As adding a lot of machines might take some time, ensure that the GUI (and progress message) is updated
|
||||||
|
CuraApplication.getInstance().processEvents()
|
||||||
|
|
||||||
# Inform the Cloud printers model about new devices.
|
# Inform the Cloud printers model about new devices.
|
||||||
new_devices_list_of_dicts = [{
|
new_devices_list_of_dicts = [{
|
||||||
"key": d.getId(),
|
"key": d.getId(),
|
||||||
"name": d.name,
|
"name": d.name,
|
||||||
"machine_type": d.printerTypeName,
|
"machine_type": d.printerTypeName,
|
||||||
"firmware_version": d.firmwareVersion} for d in new_devices]
|
"firmware_version": d.firmwareVersion} for d in new_output_devices]
|
||||||
discovered_cloud_printers_model = CuraApplication.getInstance().getDiscoveredCloudPrintersModel()
|
discovered_cloud_printers_model = CuraApplication.getInstance().getDiscoveredCloudPrintersModel()
|
||||||
discovered_cloud_printers_model.addDiscoveredCloudPrinters(new_devices_list_of_dicts)
|
discovered_cloud_printers_model.addDiscoveredCloudPrinters(new_devices_list_of_dicts)
|
||||||
|
|
||||||
if not new_devices:
|
if not new_output_devices:
|
||||||
if remote_clusters_added:
|
if remote_clusters_added:
|
||||||
self._connectToActiveMachine()
|
self._connectToActiveMachine()
|
||||||
return
|
return
|
||||||
|
@ -220,55 +235,29 @@ class CloudOutputDeviceManager:
|
||||||
# Sort new_devices on online status first, alphabetical second.
|
# Sort new_devices on online status first, alphabetical second.
|
||||||
# Since the first device might be activated in case there is no active printer yet,
|
# Since the first device might be activated in case there is no active printer yet,
|
||||||
# it would be nice to prioritize online devices
|
# it would be nice to prioritize online devices
|
||||||
online_cluster_names = {c.friendly_name.lower() for c in clusters if c.is_online and not c.friendly_name is None}
|
online_cluster_names = {c.friendly_name.lower() for c in discovered_clusters if c.is_online and not c.friendly_name is None}
|
||||||
new_devices.sort(key = lambda x: ("a{}" if x.name.lower() in online_cluster_names else "b{}").format(x.name.lower()))
|
new_output_devices.sort(key = lambda x: ("a{}" if x.name.lower() in online_cluster_names else "b{}").format(x.name.lower()))
|
||||||
|
|
||||||
message = Message(
|
message = NewPrinterDetectedMessage(num_printers_found = len(new_output_devices))
|
||||||
title = self.i18n_catalog.i18ncp(
|
|
||||||
"info:status",
|
|
||||||
"New printer detected from your Ultimaker account",
|
|
||||||
"New printers detected from your Ultimaker account",
|
|
||||||
len(new_devices)
|
|
||||||
),
|
|
||||||
progress = 0,
|
|
||||||
lifetime = 0,
|
|
||||||
message_type = Message.MessageType.POSITIVE
|
|
||||||
)
|
|
||||||
message.show()
|
message.show()
|
||||||
|
|
||||||
new_devices_added = []
|
new_devices_added = []
|
||||||
|
|
||||||
for idx, device in enumerate(new_devices):
|
for idx, output_device in enumerate(new_output_devices):
|
||||||
message_text = self.i18n_catalog.i18nc("info:status Filled in with printer name and printer model.", "Adding printer {name} ({model}) from your account").format(name = device.name, model = device.printerTypeName)
|
message.updateProgressText(output_device)
|
||||||
message.setText(message_text)
|
|
||||||
if len(new_devices) > 1:
|
self._remote_clusters[output_device.getId()] = output_device
|
||||||
message.setProgress((idx / len(new_devices)) * 100)
|
|
||||||
CuraApplication.getInstance().processEvents()
|
|
||||||
self._remote_clusters[device.getId()] = device
|
|
||||||
|
|
||||||
# If there is no active machine, activate the first available cloud printer
|
# If there is no active machine, activate the first available cloud printer
|
||||||
activate = not CuraApplication.getInstance().getMachineManager().activeMachine
|
activate = not CuraApplication.getInstance().getMachineManager().activeMachine
|
||||||
|
|
||||||
if self._createMachineFromDiscoveredDevice(device.getId(), activate = activate):
|
if self._createMachineFromDiscoveredDevice(output_device.getId(), activate = activate):
|
||||||
new_devices_added.append(device)
|
new_devices_added.append(output_device)
|
||||||
|
|
||||||
message.setProgress(None)
|
message.finalize(new_devices_added, new_output_devices)
|
||||||
|
|
||||||
max_disp_devices = 3
|
@staticmethod
|
||||||
if len(new_devices_added) > max_disp_devices:
|
def _updateOnlinePrinters(printer_responses: Dict[str, CloudClusterResponse]) -> None:
|
||||||
num_hidden = len(new_devices_added) - max_disp_devices
|
|
||||||
device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices[0:max_disp_devices]]
|
|
||||||
device_name_list.append("<li>" + self.i18n_catalog.i18ncp("info:{0} gets replaced by a number of printers", "... and {0} other", "... and {0} others", num_hidden) + "</li>")
|
|
||||||
device_names = "".join(device_name_list)
|
|
||||||
else:
|
|
||||||
device_names = "".join(["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices_added])
|
|
||||||
if new_devices_added:
|
|
||||||
message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>"
|
|
||||||
message.setText(message_text)
|
|
||||||
else:
|
|
||||||
message.hide()
|
|
||||||
|
|
||||||
def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None:
|
|
||||||
"""
|
"""
|
||||||
Update the metadata of the printers to store whether they are online or not.
|
Update the metadata of the printers to store whether they are online or not.
|
||||||
:param printer_responses: The responses received from the API about the printer statuses.
|
:param printer_responses: The responses received from the API about the printer statuses.
|
||||||
|
@ -291,7 +280,8 @@ class CloudOutputDeviceManager:
|
||||||
old_cluster_id = outdated_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
old_cluster_id = outdated_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
||||||
outdated_machine.setMetaDataEntry(self.META_CLUSTER_ID, new_cloud_output_device.key)
|
outdated_machine.setMetaDataEntry(self.META_CLUSTER_ID, new_cloud_output_device.key)
|
||||||
outdated_machine.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
outdated_machine.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
||||||
# Cleanup the remainings of the old CloudOutputDevice(old_cluster_id)
|
|
||||||
|
# Cleanup the remains of the old CloudOutputDevice(old_cluster_id)
|
||||||
self._um_cloud_printers[new_cloud_output_device.key] = self._um_cloud_printers.pop(old_cluster_id)
|
self._um_cloud_printers[new_cloud_output_device.key] = self._um_cloud_printers.pop(old_cluster_id)
|
||||||
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
if old_cluster_id in output_device_manager.getOutputDeviceIds():
|
if old_cluster_id in output_device_manager.getOutputDeviceIds():
|
||||||
|
@ -321,56 +311,19 @@ class CloudOutputDeviceManager:
|
||||||
for device_id in removed_device_ids:
|
for device_id in removed_device_ids:
|
||||||
if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
|
if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")):
|
||||||
ignored_device_ids.add(device_id)
|
ignored_device_ids.add(device_id)
|
||||||
|
|
||||||
# Keep the reported_device_ids list in a class variable, so that the message button actions can access it and
|
# Keep the reported_device_ids list in a class variable, so that the message button actions can access it and
|
||||||
# take the necessary steps to fulfill their purpose.
|
# take the necessary steps to fulfill their purpose.
|
||||||
self.reported_device_ids = removed_device_ids - ignored_device_ids
|
self.reported_device_ids = removed_device_ids - ignored_device_ids
|
||||||
if not self.reported_device_ids:
|
if not self.reported_device_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Generate message
|
|
||||||
self._removed_printers_message = Message(
|
|
||||||
title = self.i18n_catalog.i18ncp(
|
|
||||||
"info:status",
|
|
||||||
"A cloud connection is not available for a printer",
|
|
||||||
"A cloud connection is not available for some printers",
|
|
||||||
len(self.reported_device_ids)
|
|
||||||
),
|
|
||||||
message_type = Message.MessageType.WARNING
|
|
||||||
)
|
|
||||||
device_names = "".join(["<li>{} ({})</li>".format(self._um_cloud_printers[device].name, self._um_cloud_printers[device].definition.name) for device in self.reported_device_ids])
|
|
||||||
message_text = self.i18n_catalog.i18ncp(
|
|
||||||
"info:status",
|
|
||||||
"This printer is not linked to the Digital Factory:",
|
|
||||||
"These printers are not linked to the Digital Factory:",
|
|
||||||
len(self.reported_device_ids)
|
|
||||||
)
|
|
||||||
message_text += "<br/><ul>{}</ul><br/>".format(device_names)
|
|
||||||
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
|
|
||||||
|
|
||||||
message_text += self.i18n_catalog.i18nc(
|
|
||||||
"info:status",
|
|
||||||
"To establish a connection, please visit the {website_link}".format(website_link = "<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&utm_campaign=change-account-connect-printer'>{}</a>.".format(digital_factory_string))
|
|
||||||
)
|
|
||||||
self._removed_printers_message.setText(message_text)
|
|
||||||
self._removed_printers_message.addAction("keep_printer_configurations_action",
|
|
||||||
name = self.i18n_catalog.i18nc("@action:button", "Keep printer configurations"),
|
|
||||||
icon = "",
|
|
||||||
description = "Keep cloud printers in Ultimaker Cura when not connected to your account.",
|
|
||||||
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT)
|
|
||||||
self._removed_printers_message.addAction("remove_printers_action",
|
|
||||||
name = self.i18n_catalog.i18nc("@action:button", "Remove printers"),
|
|
||||||
icon = "",
|
|
||||||
description = "Remove cloud printer(s) which aren't linked to your account.",
|
|
||||||
button_style = Message.ActionButtonStyle.SECONDARY,
|
|
||||||
button_align = Message.ActionButtonAlignment.ALIGN_LEFT)
|
|
||||||
self._removed_printers_message.actionTriggered.connect(self._onRemovedPrintersMessageActionTriggered)
|
|
||||||
|
|
||||||
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
|
|
||||||
# Remove the output device from the printers
|
# Remove the output device from the printers
|
||||||
for device_id in removed_device_ids:
|
for device_id in removed_device_ids:
|
||||||
device = self._um_cloud_printers.get(device_id, None) # type: Optional[GlobalStack]
|
global_stack: Optional[GlobalStack] = self._um_cloud_printers.get(device_id, None)
|
||||||
if not device:
|
if not global_stack:
|
||||||
continue
|
continue
|
||||||
if device_id in output_device_manager.getOutputDeviceIds():
|
if device_id in output_device_manager.getOutputDeviceIds():
|
||||||
output_device_manager.removeOutputDevice(device_id)
|
output_device_manager.removeOutputDevice(device_id)
|
||||||
|
@ -378,12 +331,19 @@ class CloudOutputDeviceManager:
|
||||||
del self._remote_clusters[device_id]
|
del self._remote_clusters[device_id]
|
||||||
|
|
||||||
# Update the printer's metadata to mark it as not linked to the account
|
# Update the printer's metadata to mark it as not linked to the account
|
||||||
device.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False)
|
global_stack.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False)
|
||||||
|
|
||||||
|
# Generate message to show
|
||||||
|
device_names = "".join(["<li>{} ({})</li>".format(self._um_cloud_printers[device].name,
|
||||||
|
self._um_cloud_printers[device].definition.name) for device in
|
||||||
|
self.reported_device_ids])
|
||||||
|
self._removed_printers_message = RemovedPrintersMessage(self.reported_device_ids, device_names)
|
||||||
|
self._removed_printers_message.actionTriggered.connect(self._onRemovedPrintersMessageActionTriggered)
|
||||||
self._removed_printers_message.show()
|
self._removed_printers_message.show()
|
||||||
|
|
||||||
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
|
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
|
||||||
device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice]
|
""" Remove the CloudOutputDevices for printers that are offline"""
|
||||||
|
device: Optional[CloudOutputDevice] = self._remote_clusters.pop(device_id, None)
|
||||||
if not device:
|
if not device:
|
||||||
return
|
return
|
||||||
device.close()
|
device.close()
|
||||||
|
@ -392,12 +352,12 @@ class CloudOutputDeviceManager:
|
||||||
output_device_manager.removeOutputDevice(device.key)
|
output_device_manager.removeOutputDevice(device.key)
|
||||||
|
|
||||||
def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> bool:
|
def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> bool:
|
||||||
device = self._remote_clusters[key]
|
device = self._remote_clusters.get(key)
|
||||||
if not device:
|
if not device:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create a new machine.
|
# Create a new machine.
|
||||||
# We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it.
|
# We do not use MachineManager.addMachine here because we need to set the cluster ID before activating it.
|
||||||
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType, show_warning_message=False)
|
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType, show_warning_message=False)
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
Logger.error(f"Failed creating a new machine for {device.name}")
|
Logger.error(f"Failed creating a new machine for {device.name}")
|
||||||
|
@ -414,15 +374,19 @@ class CloudOutputDeviceManager:
|
||||||
|
|
||||||
def _connectToActiveMachine(self) -> None:
|
def _connectToActiveMachine(self) -> None:
|
||||||
"""Callback for when the active machine was changed by the user"""
|
"""Callback for when the active machine was changed by the user"""
|
||||||
|
|
||||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if not active_machine:
|
if not active_machine:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if we should directly connect with a "normal" CloudOutputDevice or that we should connect to an
|
||||||
|
# 'abstract' one
|
||||||
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
||||||
local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY)
|
local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY)
|
||||||
for device in list(self._remote_clusters.values()): # Make a copy of the remote devices list, to prevent modifying the list while iterating, if a device gets added asynchronously.
|
|
||||||
|
# Copy of the device list, to prevent modifying the list while iterating, if a device gets added asynchronously.
|
||||||
|
remote_cluster_copy: List[CloudOutputDevice] = list(self._remote_clusters.values())
|
||||||
|
for device in remote_cluster_copy:
|
||||||
if device.key == stored_cluster_id:
|
if device.key == stored_cluster_id:
|
||||||
# Connect to it if the stored ID matches.
|
# Connect to it if the stored ID matches.
|
||||||
self._connectToOutputDevice(device, active_machine)
|
self._connectToOutputDevice(device, active_machine)
|
||||||
|
@ -433,6 +397,14 @@ class CloudOutputDeviceManager:
|
||||||
# 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)
|
||||||
|
|
||||||
|
# Update state of all abstract output devices
|
||||||
|
remote_abstract_cluster_copy: List[CloudOutputDevice] = list(self._abstract_clusters.values())
|
||||||
|
for device in remote_abstract_cluster_copy:
|
||||||
|
if device.printerType == active_machine.definition.getId() and parseBool(active_machine.getMetaDataEntry("is_abstract_machine", False)):
|
||||||
|
self._connectToAbstractOutputDevice(device, active_machine)
|
||||||
|
elif device.key in output_device_manager.getOutputDeviceIds():
|
||||||
|
output_device_manager.removeOutputDevice(device.key)
|
||||||
|
|
||||||
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack):
|
def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack):
|
||||||
machine.setName(device.name)
|
machine.setName(device.name)
|
||||||
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
||||||
|
@ -440,13 +412,24 @@ class CloudOutputDeviceManager:
|
||||||
machine.setMetaDataEntry("group_name", device.name)
|
machine.setMetaDataEntry("group_name", device.name)
|
||||||
machine.setMetaDataEntry("group_size", device.clusterSize)
|
machine.setMetaDataEntry("group_size", device.clusterSize)
|
||||||
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
|
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
|
||||||
digital_factory_link = "<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&utm_campaign=change-account-remove-printer'>{digital_factory_string}</a>".format(digital_factory_string = digital_factory_string)
|
digital_factory_link = f"<a href='https://digitalfactory.ultimaker.com?utm_source=cura&utm_medium=software&" \
|
||||||
|
f"utm_campaign=change-account-remove-printer'>{digital_factory_string}</a>"
|
||||||
removal_warning_string = self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync.").format(printer_name = device.name) \
|
removal_warning_string = self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "{printer_name} will be removed until the next account sync.").format(printer_name = device.name) \
|
||||||
+ "<br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "To remove {printer_name} permanently, visit {digital_factory_link}").format(printer_name = device.name, digital_factory_link = digital_factory_link) \
|
+ "<br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "To remove {printer_name} permanently, visit {digital_factory_link}").format(printer_name = device.name, digital_factory_link = digital_factory_link) \
|
||||||
+ "<br><br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "Are you sure you want to remove {printer_name} temporarily?").format(printer_name = device.name)
|
+ "<br><br>" + self.i18n_catalog.i18nc("@message {printer_name} is replaced with the name of the printer", "Are you sure you want to remove {printer_name} temporarily?").format(printer_name = device.name)
|
||||||
machine.setMetaDataEntry("removal_warning", removal_warning_string)
|
machine.setMetaDataEntry("removal_warning", removal_warning_string)
|
||||||
machine.addConfiguredConnectionType(device.connectionType.value)
|
machine.addConfiguredConnectionType(device.connectionType.value)
|
||||||
|
|
||||||
|
def _connectToAbstractOutputDevice(self, device: AbstractCloudOutputDevice, machine: GlobalStack) -> None:
|
||||||
|
Logger.debug(f"Attempting to connect to abstract machine {machine.id}")
|
||||||
|
if not device.isConnected():
|
||||||
|
device.connect()
|
||||||
|
machine.addConfiguredConnectionType(device.connectionType.value)
|
||||||
|
|
||||||
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
|
if device.key not in output_device_manager.getOutputDeviceIds():
|
||||||
|
output_device_manager.addOutputDevice(device)
|
||||||
|
|
||||||
def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None:
|
def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None:
|
||||||
"""Connects to an output device and makes sure it is registered in the output device manager."""
|
"""Connects to an output device and makes sure it is registered in the output device manager."""
|
||||||
|
|
||||||
|
@ -472,7 +455,7 @@ class CloudOutputDeviceManager:
|
||||||
if container_cluster_id in self._remote_clusters.keys():
|
if container_cluster_id in self._remote_clusters.keys():
|
||||||
del self._remote_clusters[container_cluster_id]
|
del self._remote_clusters[container_cluster_id]
|
||||||
|
|
||||||
def _onRemovedPrintersMessageActionTriggered(self, removed_printers_message: Message, action: str) -> None:
|
def _onRemovedPrintersMessageActionTriggered(self, removed_printers_message: RemovedPrintersMessage, action: str) -> None:
|
||||||
if action == "keep_printer_configurations_action":
|
if action == "keep_printer_configurations_action":
|
||||||
removed_printers_message.hide()
|
removed_printers_message.hide()
|
||||||
elif action == "remove_printers_action":
|
elif action == "remove_printers_action":
|
||||||
|
@ -483,12 +466,16 @@ class CloudOutputDeviceManager:
|
||||||
question_title = self.i18n_catalog.i18nc("@title:window", "Remove printers?")
|
question_title = self.i18n_catalog.i18nc("@title:window", "Remove printers?")
|
||||||
question_content = self.i18n_catalog.i18ncp(
|
question_content = self.i18n_catalog.i18ncp(
|
||||||
"@label",
|
"@label",
|
||||||
"You are about to remove {0} printer from Cura. This action cannot be undone.\nAre you sure you want to continue?",
|
"You are about to remove {0} printer from Cura. This action cannot be undone.\n"
|
||||||
"You are about to remove {0} printers from Cura. This action cannot be undone.\nAre you sure you want to continue?",
|
"Are you sure you want to continue?",
|
||||||
|
"You are about to remove {0} printers from Cura. This action cannot be undone.\n"
|
||||||
|
"Are you sure you want to continue?",
|
||||||
len(remove_printers_ids)
|
len(remove_printers_ids)
|
||||||
)
|
)
|
||||||
if remove_printers_ids == all_ids:
|
if remove_printers_ids == all_ids:
|
||||||
question_content = self.i18n_catalog.i18nc("@label", "You are about to remove all printers from Cura. This action cannot be undone.\nAre you sure you want to continue?")
|
question_content = self.i18n_catalog.i18nc("@label", "You are about to remove all printers from Cura. "
|
||||||
|
"This action cannot be undone.\n"
|
||||||
|
"Are you sure you want to continue?")
|
||||||
result = QMessageBox.question(None, question_title, question_content)
|
result = QMessageBox.question(None, question_title, question_content)
|
||||||
if result == QMessageBox.StandardButton.No:
|
if result == QMessageBox.StandardButton.No:
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
class NewPrinterDetectedMessage(Message):
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def __init__(self, num_printers_found: int) -> None:
|
||||||
|
super().__init__(title = self.i18n_catalog.i18ncp("info:status",
|
||||||
|
"New printer detected from your Ultimaker account",
|
||||||
|
"New printers detected from your Ultimaker account",
|
||||||
|
num_printers_found),
|
||||||
|
progress = 0,
|
||||||
|
lifetime = 0,
|
||||||
|
message_type = Message.MessageType.POSITIVE)
|
||||||
|
self._printers_added = 0
|
||||||
|
self._num_printers_found = num_printers_found
|
||||||
|
|
||||||
|
def updateProgressText(self, output_device):
|
||||||
|
"""
|
||||||
|
While the progress of adding printers is running, update the text displayed.
|
||||||
|
:param output_device: The output device that is being added.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
message_text = self.i18n_catalog.i18nc("info:status Filled in with printer name and printer model.",
|
||||||
|
"Adding printer {name} ({model}) from your account").format(
|
||||||
|
name=output_device.name, model=output_device.printerTypeName)
|
||||||
|
self.setText(message_text)
|
||||||
|
if self._num_printers_found > 1:
|
||||||
|
self.setProgress((self._printers_added / self._num_printers_found) * 100)
|
||||||
|
self._printers_added += 1
|
||||||
|
|
||||||
|
CuraApplication.getInstance().processEvents()
|
||||||
|
|
||||||
|
def finalize(self, new_devices_added, new_output_devices):
|
||||||
|
self.setProgress(None)
|
||||||
|
num_devices_added = len(new_devices_added)
|
||||||
|
max_disp_devices = 3
|
||||||
|
|
||||||
|
if num_devices_added > max_disp_devices:
|
||||||
|
num_hidden = num_devices_added - max_disp_devices
|
||||||
|
device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in
|
||||||
|
new_output_devices[0: max_disp_devices]]
|
||||||
|
device_name_list.append(
|
||||||
|
"<li>" + self.i18n_catalog.i18ncp("info:{0} gets replaced by a number of printers", "... and {0} other",
|
||||||
|
"... and {0} others", num_hidden) + "</li>")
|
||||||
|
device_names = "".join(device_name_list)
|
||||||
|
else:
|
||||||
|
device_names = "".join(
|
||||||
|
["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices_added])
|
||||||
|
|
||||||
|
if new_devices_added:
|
||||||
|
message_text = self.i18n_catalog.i18nc("info:status",
|
||||||
|
"Printers added from Digital Factory:") + f"<ul>{device_names}</ul>"
|
||||||
|
self.setText(message_text)
|
||||||
|
else:
|
||||||
|
self.hide()
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
class RemovedPrintersMessage(Message):
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def __init__(self, removed_devices, device_names) -> None:
|
||||||
|
self._removed_devices = removed_devices
|
||||||
|
|
||||||
|
message_text = self.i18n_catalog.i18ncp(
|
||||||
|
"info:status",
|
||||||
|
"This printer is not linked to the Digital Factory:",
|
||||||
|
"These printers are not linked to the Digital Factory:",
|
||||||
|
len(self.removed_devices)
|
||||||
|
)
|
||||||
|
message_text += "<br/><ul>{}</ul><br/>".format(device_names)
|
||||||
|
|
||||||
|
digital_factory_string = self.i18n_catalog.i18nc("info:name", "Ultimaker Digital Factory")
|
||||||
|
website_link = f"<a href='https://digitalfactory.ultimaker.com?utm_source=cura&" \
|
||||||
|
f"utm_medium=software&utm_campaign=change-account-connect-printer'>{digital_factory_string}</a>."
|
||||||
|
|
||||||
|
message_text += self.i18n_catalog.i18nc(
|
||||||
|
"info:status",
|
||||||
|
f"To establish a connection, please visit the {website_link}"
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(title=self.i18n_catalog.i18ncp("info:status",
|
||||||
|
"A cloud connection is not available for a printer",
|
||||||
|
"A cloud connection is not available for some printers",
|
||||||
|
len(self.removed_devices)),
|
||||||
|
message_type=Message.MessageType.WARNING,
|
||||||
|
text = message_text)
|
||||||
|
|
||||||
|
self.addAction("keep_printer_configurations_action",
|
||||||
|
name=self.i18n_catalog.i18nc("@action:button",
|
||||||
|
"Keep printer configurations"),
|
||||||
|
icon="",
|
||||||
|
description="Keep cloud printers in Ultimaker Cura when not connected to your account.",
|
||||||
|
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
||||||
|
self.addAction("remove_printers_action",
|
||||||
|
name=self.i18n_catalog.i18nc("@action:button", "Remove printers"),
|
||||||
|
icon="",
|
||||||
|
description="Remove cloud printer(s) which aren't linked to your account.",
|
||||||
|
button_style=Message.ActionButtonStyle.SECONDARY,
|
||||||
|
button_align=Message.ActionButtonAlignment.ALIGN_LEFT)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ from ..BaseModel import BaseModel
|
||||||
class CloudClusterResponse(BaseModel):
|
class CloudClusterResponse(BaseModel):
|
||||||
"""Class representing a cloud connected cluster."""
|
"""Class representing a cloud connected cluster."""
|
||||||
|
|
||||||
|
|
||||||
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", printer_count: int = 1,
|
friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1,
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from .CloudClusterResponse import CloudClusterResponse
|
||||||
|
from .ClusterPrinterStatus import ClusterPrinterStatus
|
||||||
|
|
||||||
|
|
||||||
|
class CloudClusterWithConfigResponse(CloudClusterResponse):
|
||||||
|
"""Class representing a cloud connected cluster."""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
self.configuration = self.parseModel(ClusterPrinterStatus, kwargs.get("host_printer"))
|
||||||
|
super().__init__(**kwargs)
|
|
@ -20,7 +20,6 @@ from ..BaseModel import BaseModel
|
||||||
class ClusterPrinterStatus(BaseModel):
|
class ClusterPrinterStatus(BaseModel):
|
||||||
"""Class representing a cluster printer"""
|
"""Class representing a cluster 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]],
|
||||||
|
@ -28,9 +27,9 @@ 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
|
"""
|
||||||
|
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 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 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 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 ip_address: The IP address of the printer in the local network.
|
||||||
|
|
|
@ -221,7 +221,7 @@ class LocalClusterOutputDeviceManager:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a new machine and activate it.
|
# Create a new machine and activate it.
|
||||||
# We do not use use MachineManager.addMachine here because we need to set the network key before activating it.
|
# We do not use MachineManager.addMachine here because we need to set the network key before activating it.
|
||||||
# If we do not do this the auto-pairing with the cloud-equivalent device will not work.
|
# If we do not do this the auto-pairing with the cloud-equivalent device will not work.
|
||||||
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType)
|
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType)
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
|
@ -234,7 +234,6 @@ class LocalClusterOutputDeviceManager:
|
||||||
|
|
||||||
_abstract_machine = CuraStackBuilder.createAbstractMachine(device.printerType)
|
_abstract_machine = CuraStackBuilder.createAbstractMachine(device.printerType)
|
||||||
|
|
||||||
|
|
||||||
def _storeManualAddress(self, address: str) -> None:
|
def _storeManualAddress(self, address: str) -> None:
|
||||||
"""Add an address to the stored preferences."""
|
"""Add an address to the stored preferences."""
|
||||||
|
|
||||||
|
|
|
@ -221,10 +221,7 @@ Button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted:
|
Component.onCompleted: configurationItem.checked = Cura.MachineManager.matchesConfiguration(configuration)
|
||||||
{
|
|
||||||
configurationItem.checked = Cura.MachineManager.matchesConfiguration(configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked:
|
onClicked:
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue