From 9202bb11fe11a873f44b264e2674723b89bcef1d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 13:12:15 +0100 Subject: [PATCH] Added stubs for cluster & legacy output devices CL-541 --- .../ClusterUM3OutputDevice.py | 5 + .../UM3NetworkPrinting/DiscoverUM3Action.py | 8 +- .../LegacyUM3OutputDevice.py | 5 + .../UM3OutputDevicePlugin.py | 185 ++++++++++++++++++ .../UM3PrinterOutputDevicePlugin.py | 2 - plugins/UM3NetworkPrinting/__init__.py | 4 +- 6 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py create mode 100644 plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py create mode 100644 plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py delete mode 100644 plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py new file mode 100644 index 0000000000..4609e86f20 --- /dev/null +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -0,0 +1,5 @@ +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + +class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): + def __init__(self, device_id, address, properties, parent = None): + super().__init__(device_id = device_id, parent = parent) \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index af1a556892..f199f7cd24 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -37,7 +37,7 @@ class DiscoverUM3Action(MachineAction): if not self._network_plugin: Logger.log("d", "Starting printer discovery.") self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") - self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) + self._network_plugin.discoveredDevicesChanged.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() ## Re-filters the list of printers. @@ -87,10 +87,10 @@ class DiscoverUM3Action(MachineAction): else: global_printer_type = "unknown" - printers = list(self._network_plugin.getPrinters().values()) + printers = list(self._network_plugin.getDiscoveredDevices().values()) # TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet. - printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"] - printers.sort(key = lambda k: k.name) + #printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"] + #printers.sort(key = lambda k: k.name) return printers else: return [] diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py new file mode 100644 index 0000000000..0e19df4c18 --- /dev/null +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -0,0 +1,5 @@ +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + +class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): + def __init__(self, device_id, address, properties, parent = None): + super().__init__(device_id = device_id, parent = parent) \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py new file mode 100644 index 0000000000..37425bfef2 --- /dev/null +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -0,0 +1,185 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin +from UM.Logger import Logger +from UM.Application import Application +from UM.Signal import Signal, signalemitter + +from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo +from queue import Queue +from threading import Event, Thread + +from time import time + +from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice + +## 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. +# If we discover a printer that has the same key as the active machine instance a connection is made. +@signalemitter +class UM3OutputDevicePlugin(OutputDevicePlugin): + addDeviceSignal = Signal() + removeDeviceSignal = Signal() + discoveredDevicesChanged = Signal() + + def __init__(self): + super().__init__() + self._zero_conf = None + self._zero_conf_browser = None + + # Because the model needs to be created in the same thread as the QMLEngine, we use a signal. + self.addDeviceSignal.connect(self._onAddDevice) + self.removeDeviceSignal.connect(self._onRemoveDevice) + + self._discovered_devices = {} + + # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests + # which fail to get detailed service info. + # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick + # them up and process them. + self._service_changed_request_queue = Queue() + self._service_changed_request_event = Event() + self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True) + self._service_changed_request_thread.start() + + def getDiscoveredDevices(self): + return self._discovered_devices + + ## Start looking for devices on network. + def start(self): + self.startDiscovery() + + def startDiscovery(self): + self.stop() + if self._zero_conf_browser: + self._zero_conf_browser.cancel() + self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed. + + self._zero_conf = Zeroconf() + self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', + [self._appendServiceChangedRequest]) + + def stop(self): + if self._zero_conf is not None: + Logger.log("d", "zeroconf close...") + self._zero_conf.close() + + def _onRemoveDevice(self, name): + device = self._discovered_devices.pop(name, None) + if device: + if device.isConnected(): + device.disconnect() + device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + + self.discoveredDevicesChanged.emit() + '''printer = self._printers.pop(name, None) + if printer: + if printer.isConnected(): + printer.disconnect() + printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged) + Logger.log("d", "removePrinter, disconnecting [%s]..." % name) + self.printerListChanged.emit()''' + + def _onAddDevice(self, name, address, properties): + + # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster" + # or "Legacy" UM3 device. + cluster_size = int(properties.get(b"cluster_size", -1)) + if cluster_size > 0: + device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) + else: + device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) + + self._discovered_devices[device.getId()] = device + self.discoveredDevicesChanged.emit() + + pass + ''' + self._cluster_printers_seen[ + printer.getKey()] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"): + if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced? + Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey()) + self._printers[printer.getKey()].connect() + printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged) + self.printerListChanged.emit()''' + + ## Appends a service changed request so later the handling thread will pick it up and processes it. + def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change): + # append the request and set the event so the event handling thread can pick it up + item = (zeroconf, service_type, name, state_change) + self._service_changed_request_queue.put(item) + self._service_changed_request_event.set() + + def _handleOnServiceChangedRequests(self): + while True: + # Wait for the event to be set + self._service_changed_request_event.wait(timeout = 5.0) + + # Stop if the application is shutting down + if Application.getInstance().isShuttingDown(): + return + + self._service_changed_request_event.clear() + + # Handle all pending requests + reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled + while not self._service_changed_request_queue.empty(): + request = self._service_changed_request_queue.get() + zeroconf, service_type, name, state_change = request + try: + result = self._onServiceChanged(zeroconf, service_type, name, state_change) + if not result: + reschedule_requests.append(request) + except Exception: + Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled", + service_type, name) + reschedule_requests.append(request) + + # Re-schedule the failed requests if any + if reschedule_requests: + for request in reschedule_requests: + self._service_changed_request_queue.put(request) + + ## Handler for zeroConf detection. + # Return True or False indicating if the process succeeded. + # Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread. + def _onServiceChanged(self, zero_conf, service_type, name, state_change): + if state_change == ServiceStateChange.Added: + Logger.log("d", "Bonjour service added: %s" % name) + + # First try getting info from zero-conf cache + info = ServiceInfo(service_type, name, properties={}) + for record in zero_conf.cache.entries_with_name(name.lower()): + info.update_record(zero_conf, time(), record) + + for record in zero_conf.cache.entries_with_name(info.server): + info.update_record(zero_conf, time(), record) + if info.address: + break + + # Request more data if info is not complete + if not info.address: + Logger.log("d", "Trying to get address of %s", name) + info = zero_conf.get_service_info(service_type, name) + + if info: + type_of_device = info.properties.get(b"type", None) + if type_of_device: + if type_of_device == b"printer": + address = '.'.join(map(lambda n: str(n), info.address)) + self.addDeviceSignal.emit(str(name), address, info.properties) + else: + Logger.log("w", + "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device) + else: + Logger.log("w", "Could not get information about %s" % name) + return False + + elif state_change == ServiceStateChange.Removed: + Logger.log("d", "Bonjour service removed: %s" % name) + self.removeDeviceSignal.emit(str(name)) + + return True \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py deleted file mode 100644 index 828fe76b64..0000000000 --- a/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index 37f863bd00..6dd86a16d2 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -5,8 +5,10 @@ from . import DiscoverUM3Action from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") +from . import UM3OutputDevicePlugin + def getMetaData(): return {} def register(app): - return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()} \ No newline at end of file + return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()} \ No newline at end of file