Extract zeroconf into separate class

This commit is contained in:
ChrisTerBeke 2019-07-29 21:42:52 +02:00
parent 4b71b45aa3
commit 25fde1e0c4
2 changed files with 145 additions and 128 deletions

View file

@ -1,12 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 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 queue import Queue
from threading import Thread, Event
from time import time
from typing import Dict, Optional, Callable from typing import Dict, Optional, Callable
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from UM import i18nCatalog from UM import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import Signal from UM.Signal import Signal
@ -15,6 +10,7 @@ from UM.Version import Version
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from plugins.UM3NetworkPrinting.src.Network.ZeroConfClient import ZeroConfClient
from .ClusterApiClient import ClusterApiClient from .ClusterApiClient import ClusterApiClient
from .NetworkOutputDevice import NetworkOutputDevice from .NetworkOutputDevice import NetworkOutputDevice
@ -23,7 +19,6 @@ from .NetworkOutputDevice import NetworkOutputDevice
## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters. ## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters.
class NetworkOutputDeviceManager: class NetworkOutputDeviceManager:
ZERO_CONF_NAME = u"_ultimaker._tcp.local."
MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances" MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances"
MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0") MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0")
@ -33,47 +28,24 @@ class NetworkOutputDeviceManager:
# Signal emitted when the list of discovered devices changed. # Signal emitted when the list of discovered devices changed.
discoveredDevicesChanged = Signal() discoveredDevicesChanged = Signal()
# Signals emitted when new services were discovered or removed on the network.
addedNetworkCluster = Signal()
removedNetworkCluster = Signal()
def __init__(self) -> None: def __init__(self) -> None:
# Persistent dict containing the networked clusters. # Persistent dict containing the networked clusters.
self._discovered_devices = {} # type: Dict[str, NetworkOutputDevice] self._discovered_devices = {} # type: Dict[str, NetworkOutputDevice]
self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
# TODO: move zeroconf stuff to own class? # Hook up ZeroConf client.
self._zero_conf = None # type: Optional[Zeroconf] self._zero_conf_client = ZeroConfClient()
self._zero_conf_browser = None # type: Optional[ServiceBrowser] self._zero_conf_client.addedNetworkCluster.connect(self._onAddDevice)
self._service_changed_request_queue = None # type: Optional[Queue] self._zero_conf_client.removedNetworkCluster.connect(self._onRemoveDevice)
self._service_changed_request_event = None # type: Optional[Event]
self._service_changed_request_thread = None # type: Optional[Thread]
# TODO: move manual device stuff to own class? # TODO: move manual device stuff to own class?
# Persistent dict containing manually connected clusters. # Persistent dict containing manually connected clusters.
self._manual_instances = {} # type: Dict[str, Callable] self._manual_instances = {} # type: Dict[str, Callable]
# Hook up the signals for discovery.
self.addedNetworkCluster.connect(self._onAddDevice)
self.removedNetworkCluster.connect(self._onRemoveDevice)
## Start the network discovery. ## Start the network discovery.
def start(self): def start(self) -> None:
# The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. self._zero_conf_client.start()
# We can also re-schedule the requests when they fail to get detailed service info.
# Any new or re-reschedule requests will be appended to the request queue and the thread will 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()
# Start network discovery.
self.stop()
self._zero_conf = Zeroconf()
self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [
self._appendServiceChangedRequest
])
# Load all manual devices. # Load all manual devices.
self._manual_instances = self._getStoredManualInstances() self._manual_instances = self._getStoredManualInstances()
@ -81,14 +53,8 @@ class NetworkOutputDeviceManager:
self.addManualDevice(address) self.addManualDevice(address)
## Stop network discovery and clean up discovered devices. ## Stop network discovery and clean up discovered devices.
def stop(self): def stop(self) -> None:
# Cleanup ZeroConf resources. self._zero_conf_client.stop()
if self._zero_conf is not None:
self._zero_conf.close()
self._zero_conf = None
if self._zero_conf_browser is not None:
self._zero_conf_browser.cancel()
self._zero_conf_browser = None
# Cleanup all manual devices. # Cleanup all manual devices.
for instance_name in list(self._discovered_devices): for instance_name in list(self._discovered_devices):
@ -254,88 +220,3 @@ class NetworkOutputDeviceManager:
@staticmethod @staticmethod
def _onApiError(errors) -> None: def _onApiError(errors) -> None:
Logger.log("w", str(errors)) Logger.log("w", str(errors))
## Appends a service changed request so later the handling thread will pick it up and processes it.
def _appendServiceChangedRequest(self, zeroconf: Zeroconf, service_type, name: str,
state_change: ServiceStateChange) -> None:
item = (zeroconf, service_type, name, state_change)
self._service_changed_request_queue.put(item)
self._service_changed_request_event.set()
def _handleOnServiceChangedRequests(self) -> None:
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 CuraApplication.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 careful calling it from the main thread.
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange
) -> bool:
if state_change == ServiceStateChange.Added:
return self._onServiceAdded(zero_conf, service_type, name)
elif state_change == ServiceStateChange.Removed:
return self._onServiceRemoved(name)
return True
## Handler for when a ZeroConf service was added.
def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool:
# 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:
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.addedNetworkCluster.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
return True
## Handler for when a ZeroConf service was removed.
def _onServiceRemoved(self, name: str) -> bool:
Logger.log("d", "ZeroConf service removed: %s" % name)
self.removedNetworkCluster.emit(str(name))
return True

View file

@ -0,0 +1,136 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from queue import Queue
from threading import Thread, Event
from time import time
from typing import Optional
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from UM.Logger import Logger
from UM.Signal import Signal
from cura.CuraApplication import CuraApplication
## The ZeroConfClient handles all network discovery logic.
# It emits signals when new network services were found or disappeared.
class ZeroConfClient:
# The discovery protocol name for Ultimaker printers.
ZERO_CONF_NAME = u"_ultimaker._tcp.local."
# Signals emitted when new services were discovered or removed on the network.
addedNetworkCluster = Signal()
removedNetworkCluster = Signal()
def __init__(self) -> None:
self._zero_conf = None # type: Optional[Zeroconf]
self._zero_conf_browser = None # type: Optional[ServiceBrowser]
self._service_changed_request_queue = None # type: Optional[Queue]
self._service_changed_request_event = None # type: Optional[Event]
self._service_changed_request_thread = None # type: Optional[Thread]
## The ZeroConf service changed requests are handled in a separate thread so we don't block the UI.
# We can also re-schedule the requests when they fail to get detailed service info.
# Any new or re-reschedule requests will be appended to the request queue and the thread will process them.
def start(self) -> None:
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()
self._zero_conf = Zeroconf()
self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [self._queueService])
# Cleanup ZeroConf resources.
def stop(self) -> None:
if self._zero_conf is not None:
self._zero_conf.close()
self._zero_conf = None
if self._zero_conf_browser is not None:
self._zero_conf_browser.cancel()
self._zero_conf_browser = None
## Handles a change is discovered network services.
def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None:
item = (zeroconf, service_type, name, state_change)
self._service_changed_request_queue.put(item)
self._service_changed_request_event.set()
## Callback for when a ZeroConf service has changes.
def _handleOnServiceChangedRequests(self) -> None:
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 CuraApplication.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 careful calling it from the main thread.
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange
) -> bool:
if state_change == ServiceStateChange.Added:
return self._onServiceAdded(zero_conf, service_type, name)
elif state_change == ServiceStateChange.Removed:
return self._onServiceRemoved(name)
return True
## Handler for when a ZeroConf service was added.
def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool:
# 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:
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.addedNetworkCluster.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
return True
## Handler for when a ZeroConf service was removed.
def _onServiceRemoved(self, name: str) -> bool:
Logger.log("d", "ZeroConf service removed: %s" % name)
self.removedNetworkCluster.emit(str(name))
return True