diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index bd916f06fc..ea0f69639d 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -1,6 +1,7 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from .src import UM3OutputDevicePlugin +from .src import UltimakerNetworkedPrinterAction def getMetaData(): @@ -9,5 +10,6 @@ def getMetaData(): def register(app): return { - "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin() + "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), + "machine_action": UltimakerNetworkedPrinterAction.UltimakerNetworkedPrinterAction() } diff --git a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml index 1a843ba200..f600083f36 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml @@ -23,37 +23,13 @@ Cura.MachineAction function connectToPrinter() { - if(base.selectedDevice && base.completeProperties) + if (base.selectedDevice && base.completeProperties) { - var printerKey = base.selectedDevice.key - var printerName = base.selectedDevice.name // TODO To change when the groups have a name - if (manager.getStoredKey() != printerKey) - { - // Check if there is another instance with the same key - if (!manager.existsKey(printerKey)) - { - manager.associateActiveMachineWithPrinterDevice(base.selectedDevice) - manager.setGroupName(printerName) // TODO To change when the groups have a name - completed() - } - else - { - existingConnectionDialog.open() - } - } + manager.associateActiveMachineWithPrinterDevice(base.selectedDevice) + completed() } } - MessageDialog - { - id: existingConnectionDialog - title: catalog.i18nc("@window:title", "Existing Connection") - icon: StandardIcon.Information - text: catalog.i18nc("@message:text", "This printer/group is already added to Cura. Please select another printer/group.") - standardButtons: StandardButton.Ok - modality: Qt.ApplicationModal - } - Column { anchors.fill: parent; @@ -151,21 +127,6 @@ Cura.MachineAction { id: listview model: manager.foundDevices - onModelChanged: - { - var selectedKey = manager.getLastManualEntryKey() - // If there is no last manual entry key, then we select the stored key (if any) - if (selectedKey == "") - selectedKey = manager.getStoredKey() - for(var i = 0; i < model.length; i++) { - if(model[i].key == selectedKey) - { - currentIndex = i; - return - } - } - currentIndex = -1; - } width: parent.width currentIndex: -1 onCurrentIndexChanged: diff --git a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml deleted file mode 100644 index 1da7c12e29..0000000000 --- a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 -import QtQuick.Controls 1.1 -import QtQuick.Layouts 1.1 -import QtQuick.Window 2.1 -import UM 1.2 as UM -import Cura 1.0 as Cura - -Item { - id: base; - property string activeQualityDefinitionId: Cura.MachineManager.activeQualityDefinitionId; - property bool isUM3: activeQualityDefinitionId == "ultimaker3" || activeQualityDefinitionId.match("ultimaker_") != null; - property bool printerConnected: Cura.MachineManager.printerConnected; - property bool printerAcceptsCommands: - { - if (printerConnected && Cura.MachineManager.printerOutputDevices[0]) - { - return Cura.MachineManager.printerOutputDevices[0].acceptsCommands - } - return false - } - property bool authenticationRequested: - { - if (printerConnected && Cura.MachineManager.printerOutputDevices[0]) - { - var device = Cura.MachineManager.printerOutputDevices[0] - // AuthState.AuthenticationRequested or AuthState.AuthenticationReceived - return device.authenticationState == 2 || device.authenticationState == 5 - } - return false - } - property var materialNames: - { - if (printerConnected && Cura.MachineManager.printerOutputDevices[0]) - { - return Cura.MachineManager.printerOutputDevices[0].materialNames - } - return null - } - property var hotendIds: - { - if (printerConnected && Cura.MachineManager.printerOutputDevices[0]) - { - return Cura.MachineManager.printerOutputDevices[0].hotendIds - } - return null - } - - UM.I18nCatalog { - id: catalog; - name: "cura"; - } - - Row { - objectName: "networkPrinterConnectButton"; - spacing: UM.Theme.getSize("default_margin").width; - visible: isUM3; - - Button { - height: UM.Theme.getSize("save_button_save_to_button").height; - onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication(); - style: UM.Theme.styles.print_setup_action_button; - text: catalog.i18nc("@action:button", "Request Access"); - tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer"); - visible: printerConnected && !printerAcceptsCommands && !authenticationRequested; - } - - Button { - height: UM.Theme.getSize("save_button_save_to_button").height; - onClicked: connectActionDialog.show(); - style: UM.Theme.styles.print_setup_action_button; - text: catalog.i18nc("@action:button", "Connect"); - tooltip: catalog.i18nc("@info:tooltip", "Connect to a printer"); - visible: !printerConnected; - } - } - - UM.Dialog { - id: connectActionDialog; - rightButtons: Button { - iconName: "dialog-close"; - onClicked: connectActionDialog.reject(); - text: catalog.i18nc("@action:button", "Close"); - } - - Loader { - anchors.fill: parent; - source: "DiscoverUM3Action.qml"; - } - } -} diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index efb529faa7..64a806e3a7 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -16,7 +16,6 @@ from .LocalClusterOutputDevice import LocalClusterOutputDevice from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice from ..Messages.CloudFlowMessage import CloudFlowMessage from ..Messages.LegacyDeviceNoLongerSupportedMessage import LegacyDeviceNoLongerSupportedMessage -from ..Messages.NotClusterHostMessage import NotClusterHostMessage from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus @@ -86,6 +85,17 @@ class LocalClusterOutputDeviceManager: def refreshConnections(self) -> None: self._connectToActiveMachine() + ## Get the discovered devices. + def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: + return self._discovered_devices + + ## Connect the active machine to a given device. + def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + active_machine = CuraApplication.getInstance().getGlobalContainerStack() + if not active_machine: + return + self._connectToOutputDevice(device, active_machine) + ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: active_machine = CuraApplication.getInstance().getGlobalContainerStack() @@ -176,8 +186,6 @@ class LocalClusterOutputDeviceManager: active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return - active_machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) - active_machine.setMetaDataEntry("group_name", device.name) self._connectToOutputDevice(device, active_machine) CloudFlowMessage(device.ipAddress).show() # Nudge the user to start using Ultimaker Cloud. @@ -215,6 +223,10 @@ class LocalClusterOutputDeviceManager: LegacyDeviceNoLongerSupportedMessage().show() return + machine.setName(device.name) + machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) + machine.setMetaDataEntry("group_name", device.name) + device.connect() machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 20388ac491..3ab37297b5 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -1,24 +1,30 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Callable +from typing import Optional, Callable, Dict +from UM.Signal import Signal from cura.CuraApplication import CuraApplication from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin +from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice from .Network.LocalClusterOutputDeviceManager import LocalClusterOutputDeviceManager from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager ## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing. class UM3OutputDevicePlugin(OutputDevicePlugin): + + # Signal emitted when the list of discovered devices changed. Used by printer action in this plugin. + discoveredDevicesChanged = Signal() def __init__(self) -> None: super().__init__() # Create a network output device manager that abstracts all network connection logic away. self._network_output_device_manager = LocalClusterOutputDeviceManager() + self._network_output_device_manager.discoveredDevicesChanged.connect(self.discoveredDevicesChanged) # Create a cloud output device manager that abstracts all cloud connection logic away. self._cloud_output_device_manager = CloudOutputDeviceManager() @@ -57,3 +63,11 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): ## Remove a manually connected networked printer. def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: self._network_output_device_manager.removeManualDevice(key, address) + + ## Get the discovered devices from the local network. + def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: + return self._network_output_device_manager.getDiscoveredDevices() + + ## Connect the active machine to a device. + def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + self._network_output_device_manager.associateActiveMachineWithPrinterDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py new file mode 100644 index 0000000000..5a37e1aeba --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py @@ -0,0 +1,91 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional, cast + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QObject + +from UM import i18nCatalog +from cura.CuraApplication import CuraApplication +from cura.MachineAction import MachineAction + +from .UM3OutputDevicePlugin import UM3OutputDevicePlugin +from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice + + +I18N_CATALOG = i18nCatalog("cura") + + +## Machine action that allows to connect the active machine to a networked devices. +# TODO: in the future this should be part of the new discovery workflow baked into Cura. +class UltimakerNetworkedPrinterAction(MachineAction): + + # Signal emitted when discovered devices have changed. + discoveredDevicesChanged = pyqtSignal() + + def __init__(self) -> None: + super().__init__("DiscoverUM3Action", I18N_CATALOG.i18nc("@action", "Connect via Network")) + self._qml_url = "resources/qml/DiscoverUM3Action.qml" + self._network_plugin = None # type: Optional[UM3OutputDevicePlugin] + + ## Override the default value. + def needsUserInteraction(self) -> bool: + return False + + ## Start listening to network discovery events via the plugin. + @pyqtSlot(name = "startDiscovery") + def startDiscovery(self) -> None: + network_plugin = self._getNetworkPlugin() + network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) + self.discoveredDevicesChanged.emit() # trigger at least once to populate the list + + ## Reset the discovered devices. + @pyqtSlot(name = "reset") + def reset(self) -> None: + self.restartDiscovery() + + ## Reset the discovered devices. + @pyqtSlot(name = "restartDiscovery") + def restartDiscovery(self) -> None: + network_plugin = self._getNetworkPlugin() + network_plugin.startDiscovery() + self.discoveredDevicesChanged.emit() # trigger to reset the list + + ## Remove a manually added device. + @pyqtSlot(str, str, name = "removeManualDevice") + def removeManualDevice(self, key: str, address: str) -> None: + network_plugin = self._getNetworkPlugin() + network_plugin.removeManualDevice(key, address) + + ## Add a new manual device. Can replace an existing one by key. + @pyqtSlot(str, str, name = "setManualDevice") + def setManualDevice(self, key: str, address: str) -> None: + network_plugin = self._getNetworkPlugin() + if key != "": + network_plugin.removeManualDevice(key) + if address != "": + network_plugin.addManualDevice(address) + + ## Get the devices discovered in the local network sorted by name. + @pyqtProperty("QVariantList", notify = discoveredDevicesChanged) + def foundDevices(self): + network_plugin = self._getNetworkPlugin() + discovered_devices = list(network_plugin.getDiscoveredDevices().values()) + discovered_devices.sort(key = lambda d: d.name) + return discovered_devices + + ## Connect a device selected in the list with the active machine. + @pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice") + def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + network_plugin = self._getNetworkPlugin() + network_plugin.associateActiveMachineWithPrinterDevice(device) + + ## Callback for when the list of discovered devices in the plugin was changed. + def _onDeviceDiscoveryChanged(self) -> None: + self.discoveredDevicesChanged.emit() + + ## Get the network manager from the plugin. + def _getNetworkPlugin(self) -> UM3OutputDevicePlugin: + if not self._network_plugin: + plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") + self._network_plugin = cast(UM3OutputDevicePlugin, plugin) + return self._network_plugin diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 4752c88f49..6f5b0a8143 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -52,6 +52,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Keeps track of all printers in the cluster. self._printers = [] # type: List[PrinterOutputModel] + self._received_printers = False # Keeps track of all print jobs in the cluster. self._print_jobs = [] # type: List[UM3PrintJobOutputModel] @@ -96,6 +97,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Get the amount of printers in the cluster. @pyqtProperty(int, notify=_clusterPrintersChanged) def clusterSize(self) -> int: + if not self._received_printers: + return 1 # prevent false positives when discovering new devices return len(self._printers) # Get the amount of printer in the cluster per type. @@ -217,6 +220,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self.setActivePrinter(None) self._printers = new_printers + self._received_printers = True if self._printers and not self.activePrinter: self.setActivePrinter(self._printers[0])