mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-13 09:47:50 -06:00
Merge pull request #7443 from Ultimaker/CURA-7055_auto_add_cloud_printers
CURA-7055_auto_add_cloud_printers
This commit is contained in:
commit
6bbfd14d00
1 changed files with 115 additions and 32 deletions
|
@ -1,26 +1,29 @@
|
||||||
# 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 os
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
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.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from cura.API import Account
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
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 .CloudApiClient import CloudApiClient
|
from .CloudApiClient import CloudApiClient
|
||||||
from .CloudOutputDevice import CloudOutputDevice
|
from .CloudOutputDevice import CloudOutputDevice
|
||||||
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
|
|
||||||
|
|
||||||
## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
|
||||||
# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
|
||||||
# API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
|
||||||
class CloudOutputDeviceManager:
|
class CloudOutputDeviceManager:
|
||||||
|
"""The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
||||||
|
|
||||||
|
Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
||||||
|
API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
||||||
|
"""
|
||||||
|
|
||||||
META_CLUSTER_ID = "um_cloud_cluster_id"
|
META_CLUSTER_ID = "um_cloud_cluster_id"
|
||||||
META_NETWORK_KEY = "um_network_key"
|
META_NETWORK_KEY = "um_network_key"
|
||||||
|
@ -44,14 +47,16 @@ class CloudOutputDeviceManager:
|
||||||
# Create a timer to update the remote cluster list
|
# Create a timer to update the remote cluster list
|
||||||
self._update_timer = QTimer()
|
self._update_timer = QTimer()
|
||||||
self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
|
self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
|
||||||
self._update_timer.setSingleShot(False)
|
# The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
self._update_timer.timeout.connect(self._getRemoteClusters)
|
self._update_timer.timeout.connect(self._getRemoteClusters)
|
||||||
|
|
||||||
# Ensure we don't start twice.
|
# Ensure we don't start twice.
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
## Starts running the cloud output device manager, thus periodically requesting cloud data.
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Starts running the cloud output device manager, thus periodically requesting cloud data."""
|
||||||
|
|
||||||
if self._running:
|
if self._running:
|
||||||
return
|
return
|
||||||
if not self._account.isLoggedIn:
|
if not self._account.isLoggedIn:
|
||||||
|
@ -61,8 +66,9 @@ class CloudOutputDeviceManager:
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
self._getRemoteClusters()
|
self._getRemoteClusters()
|
||||||
|
|
||||||
## Stops running the cloud output device manager.
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""Stops running the cloud output device manager."""
|
||||||
|
|
||||||
if not self._running:
|
if not self._running:
|
||||||
return
|
return
|
||||||
self._running = False
|
self._running = False
|
||||||
|
@ -70,47 +76,121 @@ class CloudOutputDeviceManager:
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices.
|
self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices.
|
||||||
|
|
||||||
## Force refreshing connections.
|
|
||||||
def refreshConnections(self) -> None:
|
def refreshConnections(self) -> None:
|
||||||
|
"""Force refreshing connections."""
|
||||||
|
|
||||||
self._connectToActiveMachine()
|
self._connectToActiveMachine()
|
||||||
|
|
||||||
## Called when the uses logs in or out
|
|
||||||
def _onLoginStateChanged(self, is_logged_in: bool) -> None:
|
def _onLoginStateChanged(self, is_logged_in: bool) -> None:
|
||||||
|
"""Called when the uses logs in or out"""
|
||||||
|
|
||||||
if is_logged_in:
|
if is_logged_in:
|
||||||
self.start()
|
self.start()
|
||||||
else:
|
else:
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
## Gets all remote clusters from the API.
|
|
||||||
def _getRemoteClusters(self) -> None:
|
def _getRemoteClusters(self) -> None:
|
||||||
|
"""Gets all remote clusters from the API."""
|
||||||
|
|
||||||
self._api.getClusters(self._onGetRemoteClustersFinished)
|
self._api.getClusters(self._onGetRemoteClustersFinished)
|
||||||
|
|
||||||
## Callback for when the request for getting the clusters is finished.
|
|
||||||
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
|
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
|
||||||
|
"""Callback for when the request for getting the clusters is finished."""
|
||||||
|
|
||||||
|
new_clusters = []
|
||||||
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
|
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
|
||||||
for device_id, cluster_data in online_clusters.items():
|
for device_id, cluster_data in online_clusters.items():
|
||||||
if device_id not in self._remote_clusters:
|
if device_id not in self._remote_clusters:
|
||||||
self._onDeviceDiscovered(cluster_data)
|
new_clusters.append(cluster_data)
|
||||||
else:
|
else:
|
||||||
self._onDiscoveredDeviceUpdated(cluster_data)
|
self._onDiscoveredDeviceUpdated(cluster_data)
|
||||||
|
|
||||||
|
self._onDevicesDiscovered(new_clusters)
|
||||||
|
|
||||||
removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys())
|
removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys())
|
||||||
for device_id in removed_device_keys:
|
for device_id in removed_device_keys:
|
||||||
self._onDiscoveredDeviceRemoved(device_id)
|
self._onDiscoveredDeviceRemoved(device_id)
|
||||||
|
|
||||||
def _onDeviceDiscovered(self, cluster_data: CloudClusterResponse) -> None:
|
if new_clusters or removed_device_keys:
|
||||||
device = CloudOutputDevice(self._api, cluster_data)
|
|
||||||
CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter(
|
|
||||||
ip_address=device.key,
|
|
||||||
key=device.getId(),
|
|
||||||
name=device.getName(),
|
|
||||||
create_callback=self._createMachineFromDiscoveredDevice,
|
|
||||||
machine_type=device.printerType,
|
|
||||||
device=device
|
|
||||||
)
|
|
||||||
self._remote_clusters[device.getId()] = device
|
|
||||||
self.discoveredDevicesChanged.emit()
|
self.discoveredDevicesChanged.emit()
|
||||||
|
if removed_device_keys:
|
||||||
|
# If the removed device was active we should connect to the new active device
|
||||||
self._connectToActiveMachine()
|
self._connectToActiveMachine()
|
||||||
|
# Schedule a new update
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
|
def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None:
|
||||||
|
"""**Synchronously** create machines for discovered devices
|
||||||
|
|
||||||
|
Any new machines are made available to the user.
|
||||||
|
May take a long time to complete. As this code needs access to the Application
|
||||||
|
and blocks the GIL, creating a Job for this would not make sense.
|
||||||
|
Shows a Message informing the user of progress.
|
||||||
|
"""
|
||||||
|
new_devices = []
|
||||||
|
for cluster_data in clusters:
|
||||||
|
device = CloudOutputDevice(self._api, cluster_data)
|
||||||
|
# Create a machine if we don't already have it. Do not make it the active machine.
|
||||||
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
|
||||||
|
# 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 \
|
||||||
|
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.
|
||||||
|
new_devices.append(device)
|
||||||
|
|
||||||
|
if not new_devices:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_devices.sort(key = lambda x: x.name.lower())
|
||||||
|
|
||||||
|
image_path = os.path.join(
|
||||||
|
CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "",
|
||||||
|
"resources", "svg", "cloud-flow-completed.svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
message = Message(
|
||||||
|
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,
|
||||||
|
image_source = image_path
|
||||||
|
)
|
||||||
|
message.show()
|
||||||
|
|
||||||
|
for idx, device in enumerate(new_devices):
|
||||||
|
message_text = self.I18N_CATALOG.i18nc(
|
||||||
|
"info:status", "Adding printer {} ({}) from your account",
|
||||||
|
device.name,
|
||||||
|
device.printerTypeName
|
||||||
|
)
|
||||||
|
message.setText(message_text)
|
||||||
|
if len(new_devices) > 1:
|
||||||
|
message.setProgress((idx / len(new_devices)) * 100)
|
||||||
|
CuraApplication.getInstance().processEvents()
|
||||||
|
self._remote_clusters[device.getId()] = device
|
||||||
|
self._createMachineFromDiscoveredDevice(device.getId(), activate = False)
|
||||||
|
|
||||||
|
message.setProgress(None)
|
||||||
|
|
||||||
|
max_disp_devices = 3
|
||||||
|
if len(new_devices) > max_disp_devices:
|
||||||
|
num_hidden = len(new_devices) - max_disp_devices + 1
|
||||||
|
device_name_list = ["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices[0:num_hidden]]
|
||||||
|
device_name_list.append(self.I18N_CATALOG.i18nc("info:hidden list items", "- and {} others", num_hidden))
|
||||||
|
device_names = "\n".join(device_name_list)
|
||||||
|
else:
|
||||||
|
device_names = "\n".join(["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices])
|
||||||
|
|
||||||
|
message_text = self.I18N_CATALOG.i18nc(
|
||||||
|
"info:status",
|
||||||
|
"Cloud printers added from your account:\n{}",
|
||||||
|
device_names
|
||||||
|
)
|
||||||
|
message.setText(message_text)
|
||||||
|
|
||||||
def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None:
|
def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None:
|
||||||
device = self._remote_clusters.get(cluster_data.cluster_id)
|
device = self._remote_clusters.get(cluster_data.cluster_id)
|
||||||
|
@ -128,29 +208,31 @@ class CloudOutputDeviceManager:
|
||||||
if not device:
|
if not device:
|
||||||
return
|
return
|
||||||
device.close()
|
device.close()
|
||||||
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key)
|
|
||||||
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
if device.key in output_device_manager.getOutputDeviceIds():
|
if device.key in output_device_manager.getOutputDeviceIds():
|
||||||
output_device_manager.removeOutputDevice(device.key)
|
output_device_manager.removeOutputDevice(device.key)
|
||||||
self.discoveredDevicesChanged.emit()
|
|
||||||
|
|
||||||
def _createMachineFromDiscoveredDevice(self, key: str) -> None:
|
def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> None:
|
||||||
device = self._remote_clusters[key]
|
device = self._remote_clusters[key]
|
||||||
if not device:
|
if not device:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a new machine and activate it.
|
# 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 use MachineManager.addMachine here because we need to set the cluster ID before activating it.
|
||||||
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType)
|
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType)
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
Logger.log("e", "Failed creating a new machine")
|
Logger.log("e", "Failed creating a new machine")
|
||||||
return
|
return
|
||||||
new_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
new_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
||||||
|
|
||||||
|
if activate:
|
||||||
CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId())
|
CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId())
|
||||||
|
|
||||||
self._connectToOutputDevice(device, new_machine)
|
self._connectToOutputDevice(device, new_machine)
|
||||||
|
|
||||||
## Callback for when the active machine was changed by the user or a new remote cluster was found.
|
|
||||||
def _connectToActiveMachine(self) -> None:
|
def _connectToActiveMachine(self) -> None:
|
||||||
|
"""Callback for when the active machine was changed by the user"""
|
||||||
|
|
||||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if not active_machine:
|
if not active_machine:
|
||||||
return
|
return
|
||||||
|
@ -169,8 +251,9 @@ 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)
|
||||||
|
|
||||||
## Connects to an output device and makes sure it is registered in the output device manager.
|
|
||||||
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."""
|
||||||
|
|
||||||
machine.setName(device.name)
|
machine.setName(device.name)
|
||||||
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
||||||
machine.setMetaDataEntry("group_name", device.name)
|
machine.setMetaDataEntry("group_name", device.name)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue