diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py
index 0aafbd82c7..b250b27cd3 100755
--- a/cura/Settings/MachineManager.py
+++ b/cura/Settings/MachineManager.py
@@ -517,6 +517,18 @@ class MachineManager(QObject):
return self._global_container_stack.getId()
return ""
+ @pyqtProperty(str, notify = globalContainerChanged)
+ def activeMachineFirmwareVersion(self) -> str:
+ if not self._printer_output_devices[0]:
+ return ""
+ return self._printer_output_devices[0].firmwareVersion
+
+ @pyqtProperty(str, notify = globalContainerChanged)
+ def activeMachineAddress(self) -> str:
+ if not self._printer_output_devices[0]:
+ return ""
+ return self._printer_output_devices[0].address
+
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def printerConnected(self) -> bool:
return bool(self._printer_output_devices)
diff --git a/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-completed.svg b/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-completed.svg
new file mode 100644
index 0000000000..8eba62ecc8
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-completed.svg
@@ -0,0 +1,27 @@
+
+
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-start.svg b/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-start.svg
new file mode 100644
index 0000000000..746dc269fd
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-start.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
index 78944d954b..e081beb99c 100644
--- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
@@ -7,6 +7,7 @@ from PyQt5.QtCore import QTimer
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
+from UM.Signal import Signal, signalemitter
from cura.API import Account
from cura.CuraApplication import CuraApplication
from cura.Settings.GlobalStack import GlobalStack
@@ -31,14 +32,17 @@ class CloudOutputDeviceManager:
# The translation catalog for this device.
I18N_CATALOG = i18nCatalog("cura")
+ addedCloudCluster = Signal()
+ removedCloudCluster = Signal()
+
def __init__(self) -> None:
# Persistent dict containing the remote clusters for the authenticated user.
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
- application = CuraApplication.getInstance()
- self._output_device_manager = application.getOutputDeviceManager()
+ self._application = CuraApplication.getInstance()
+ self._output_device_manager = self._application.getOutputDeviceManager()
- self._account = application.getCuraAPI().account # type: Account
+ self._account = self._application.getCuraAPI().account # type: Account
self._api = CloudApiClient(self._account, self._onApiError)
# Create a timer to update the remote cluster list
@@ -82,6 +86,7 @@ class CloudOutputDeviceManager:
removed_cluster.disconnect()
removed_cluster.close()
self._output_device_manager.removeOutputDevice(removed_cluster.key)
+ self.removedCloudCluster.emit()
del self._remote_clusters[removed_cluster.key]
# Add an output device for each new remote cluster.
@@ -89,6 +94,7 @@ class CloudOutputDeviceManager:
for added_cluster in added_clusters:
device = CloudOutputDevice(self._api, added_cluster)
self._remote_clusters[added_cluster.cluster_id] = device
+ self.addedCloudCluster.emit()
for device, cluster in updates:
device.clusterData = cluster
@@ -152,10 +158,9 @@ class CloudOutputDeviceManager:
def start(self):
if self._running:
return
- application = CuraApplication.getInstance()
self._account.loginStateChanged.connect(self._onLoginStateChanged)
# When switching machines we check if we have to activate a remote cluster.
- application.globalContainerStackChanged.connect(self._connectToActiveMachine)
+ self._application.globalContainerStackChanged.connect(self._connectToActiveMachine)
self._update_timer.timeout.connect(self._getRemoteClusters)
self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn)
@@ -163,9 +168,8 @@ class CloudOutputDeviceManager:
def stop(self):
if not self._running:
return
- application = CuraApplication.getInstance()
self._account.loginStateChanged.disconnect(self._onLoginStateChanged)
# When switching machines we check if we have to activate a remote cluster.
- application.globalContainerStackChanged.disconnect(self._connectToActiveMachine)
+ self._application.globalContainerStackChanged.disconnect(self._connectToActiveMachine)
self._update_timer.timeout.disconnect(self._getRemoteClusters)
self._onLoginStateChanged(is_logged_in = False)
diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py
index 052dd0b979..790d0c430b 100644
--- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py
+++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py
@@ -54,6 +54,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
self._api_prefix = "/cluster-api/v1/"
+ self._application = CuraApplication.getInstance()
+
self._number_of_extruders = 2
self._dummy_lambdas = (
@@ -125,7 +127,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml")
- self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
+ self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show()
@@ -211,7 +213,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Add user name to the print_job
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
- file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
+ file_name = self._application.getPrintInformation().jobName + "." + preferred_format["extension"]
output = stream.getvalue() # Either str or bytes depending on the output mode.
if isinstance(stream, io.StringIO):
@@ -250,6 +252,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._compressing_gcode = False
self._sending_gcode = False
+ ## The IP address of the printer.
+ @pyqtProperty(str, constant = True)
+ def address(self) -> str:
+ return self._address
+
def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None:
if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100
@@ -284,7 +291,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
- CuraApplication.getInstance().getController().setActiveStage("PrepareStage")
+ self._application.getController().setActiveStage("PrepareStage")
# After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
# the "reply" should be disconnected
@@ -294,7 +301,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
if action_id == "View":
- CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
+ self._application.getController().setActiveStage("MonitorStage")
@pyqtSlot()
def openPrintJobControlPanel(self) -> None:
@@ -552,7 +559,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
return result
def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel":
- material_manager = CuraApplication.getInstance().getMaterialManager()
+ material_manager = self._application.getMaterialManager()
material_group_list = None
# Avoid crashing if there is no "guid" field in the metadata
@@ -665,7 +672,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job = SendMaterialJob(device = self)
job.run()
-
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
index 7a7670d64c..891810f2f8 100644
--- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
+++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
@@ -7,17 +7,25 @@ from time import time
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
-from PyQt5.QtCore import QUrl
+from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
+from PyQt5.QtGui import QDesktopServices
from cura.CuraApplication import CuraApplication
+from cura.PrinterOutputDevice import ConnectionType
+from cura.Settings.GlobalStack import GlobalStack # typing
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter
from UM.Version import Version
+from UM.Message import Message
+from UM.i18n import i18nCatalog
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
+from typing import Optional
+
+i18n_catalog = i18nCatalog("cura")
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
# Zero-Conf is used to detect printers, which are saved in a dict.
@@ -27,6 +35,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
addDeviceSignal = Signal()
removeDeviceSignal = Signal()
discoveredDevicesChanged = Signal()
+ cloudFlowIsPossible = Signal()
def __init__(self):
super().__init__()
@@ -34,6 +43,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._zero_conf = None
self._zero_conf_browser = None
+ self._application = CuraApplication.getInstance()
+
# Create a cloud output device manager that abstracts all cloud connection logic away.
self._cloud_output_device_manager = CloudOutputDeviceManager()
@@ -41,7 +52,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self.addDeviceSignal.connect(self._onAddDevice)
self.removeDeviceSignal.connect(self._onRemoveDevice)
- CuraApplication.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
+ self._application.globalContainerStackChanged.connect(self.reCheckConnections)
self._discovered_devices = {}
@@ -49,6 +60,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._network_manager.finished.connect(self._onNetworkRequestFinished)
self._min_cluster_version = Version("4.0.0")
+ self._min_cloud_version = Version("5.2.0")
self._api_version = "1"
self._api_prefix = "/api/v" + self._api_version + "/"
@@ -74,6 +86,26 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
self._service_changed_request_thread.start()
+ self._account = self._application.getCuraAPI().account
+
+ # Check if cloud flow is possible when user logs in
+ self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)
+
+ # Check if cloud flow is possible when user switches machines
+ self._application.globalContainerStackChanged.connect(self._onMachineSwitched)
+
+ # Listen for when cloud flow is possible
+ self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)
+
+ # Listen if cloud cluster was added
+ self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured)
+
+ # Listen if cloud cluster was removed
+ self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible)
+
+ self._start_cloud_flow_message = None # type: Optional[Message]
+ self._cloud_flow_complete_message = None # type: Optional[Message]
+
def getDiscoveredDevices(self):
return self._discovered_devices
@@ -138,6 +170,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
if key == um_network_key:
self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
+ self.checkCloudFlowIsPossible()
else:
self.getOutputDeviceManager().removeOutputDevice(key)
@@ -370,3 +403,113 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self.removeDeviceSignal.emit(str(name))
return True
+
+ ## Check if the prerequsites are in place to start the cloud flow
+ def checkCloudFlowIsPossible(self) -> None:
+ Logger.log("d", "Checking if cloud connection is possible...")
+
+ # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again
+ active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
+ if active_machine:
+
+ # Check 1: Printer isn't already configured for cloud
+ if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
+ Logger.log("d", "Active machine was already configured for cloud.")
+ return
+
+ # Check 2: User did not already say "Don't ask me again"
+ if active_machine.getMetaDataEntry("show_cloud_message", "value") is False:
+ Logger.log("d", "Active machine shouldn't ask about cloud anymore.")
+ return
+
+ # Check 3: User is logged in with an Ultimaker account
+ if not self._account.isLoggedIn:
+ Logger.log("d", "Cloud Flow not possible: User not logged in!")
+ return
+
+ # Check 4: Machine is configured for network connectivity
+ if not self._application.getMachineManager().activeMachineHasActiveNetworkConnection:
+ Logger.log("d", "Cloud Flow not possible: Machine is not connected!")
+ return
+
+ # Check 5: Machine has correct firmware version
+ firmware_version = self._application.getMachineManager().activeMachineFirmwareVersion # type: str
+ if not Version(firmware_version) > self._min_cloud_version:
+ Logger.log("d", "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)",
+ firmware_version,
+ self._min_cloud_version)
+ return
+
+ Logger.log("d", "Cloud flow is possible!")
+ self.cloudFlowIsPossible.emit()
+
+ def _onCloudFlowPossible(self) -> None:
+ # Cloud flow is possible, so show the message
+ if not self._start_cloud_flow_message:
+ self._start_cloud_flow_message = Message(
+ text = i18n_catalog.i18nc("@info:status", "Pair your printer to your Ultimaker account and start print jobs from anywhere."),
+ image_source = "../../../../../Cura/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-start.svg",
+ image_caption = i18n_catalog.i18nc("@info:status", "Connect to cloud"),
+ option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
+ option_state = False
+ )
+ self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "")
+ self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain)
+ self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted)
+ self._start_cloud_flow_message.show()
+ return
+
+ def _onCloudPrintingConfigured(self) -> None:
+ if self._start_cloud_flow_message:
+ self._start_cloud_flow_message.hide()
+ self._start_cloud_flow_message = None
+
+ # Show the successful pop-up
+ if not self._start_cloud_flow_message:
+ self._cloud_flow_complete_message = Message(
+ text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."),
+ lifetime = 30,
+ image_source = "../../../../../Cura/plugins/UM3NetworkPrinting/resources/svg/cloud-flow-completed.svg",
+ image_caption = i18n_catalog.i18nc("@info:status", "Connected!")
+ )
+ self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon
+ self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection)
+ self._cloud_flow_complete_message.show()
+
+ # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
+ active_machine = self._application.getMachineManager().activeMachine
+ if active_machine:
+ active_machine.setMetaDataEntry("cloud_flow_complete", True)
+ return
+
+ def _onDontAskMeAgain(self, messageId: str, checked: bool) -> None:
+ active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
+ if active_machine:
+ active_machine.setMetaDataEntry("show_cloud_message", False)
+ Logger.log("d", "Will not ask the user again to cloud connect for current printer.")
+ return
+
+ def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
+ address = self._application.getMachineManager().activeMachineAddress # type: str
+ if address:
+ QDesktopServices.openUrl(QUrl("http://" + address + "/cloud_connect"))
+ if self._start_cloud_flow_message:
+ self._start_cloud_flow_message.hide()
+ self._start_cloud_flow_message = None
+ return
+
+ def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None:
+ address = self._application.getMachineManager().activeMachineAddress # type: str
+ if address:
+ QDesktopServices.openUrl(QUrl("http://" + address + "/settings"))
+ return
+
+ def _onMachineSwitched(self) -> None:
+ if self._start_cloud_flow_message is not None:
+ self._start_cloud_flow_message.hide()
+ self._start_cloud_flow_message = None
+ if self._cloud_flow_complete_message is not None:
+ self._cloud_flow_complete_message.hide()
+ self._cloud_flow_complete_message = None
+
+ self.checkCloudFlowIsPossible()
\ No newline at end of file