Restructure codebase - part 1

This commit is contained in:
ChrisTerBeke 2019-07-26 15:07:52 +02:00
parent 87517a77c2
commit 3c1b377308
46 changed files with 898 additions and 1725 deletions

View file

@ -0,0 +1,425 @@
from queue import Queue
from threading import Thread, Event
from time import time
from typing import Dict, Optional, Callable, List
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from UM.Logger import Logger
from UM.Signal import Signal
from UM.Version import Version
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice
from plugins.UM3NetworkPrinting.src.Network.ManualPrinterRequest import ManualPrinterRequest
## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters.
class NetworkOutputDeviceManager:
PRINTER_API_VERSION = "1"
PRINTER_API_PREFIX = "/api/v" + PRINTER_API_VERSION
CLUSTER_API_VERSION = "1"
CLUSTER_API_PREFIX = "/cluster-api/v" + CLUSTER_API_VERSION
ZERO_CONF_NAME = u"_ultimaker._tcp.local."
MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances"
MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0")
discoveredDevicesChanged = Signal()
addedNetworkCluster = Signal()
removedNetworkCluster = Signal()
def __init__(self):
# Persistent dict containing the networked clusters.
self._discovered_devices = {} # type: Dict[str, ClusterUM3OutputDevice]
self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
self._network_manager = QNetworkAccessManager()
# In order to avoid garbage collection we keep the callbacks in this list.
self._anti_gc_callbacks = [] # type: List[Callable[[], 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]
# Persistent dict containing manually connected clusters.
self._manual_instances = {} # type: Dict[str, ManualPrinterRequest]
self._last_manual_entry_key = None # type: Optional[str]
# Hook up the signals for discovery.
self.addedNetworkCluster.connect(self._onAddDevice)
self.removedNetworkCluster.connect(self._onRemoveDevice)
# # Get all discovered devices in the local network.
# def getDiscoveredDevices(self) -> Dict[str, ClusterUM3OutputDevice]:
# return self._discovered_devices
# ## Get the key of the last manually added device.
# def getLastManualDevice(self) -> str:
# return self._last_manual_entry_key
# ## Reset the last manually added device key.
# def resetLastManualDevice(self) -> None:
# self._last_manual_entry_key = ""
## Force reset all network device connections.
def refreshConnections(self):
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine:
return
um_network_key = active_machine.getMetaDataEntry("um_network_key")
for key in self._discovered_devices:
if key == um_network_key:
if not self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to connect with [%s]" % key)
# It should already be set, but if it actually connects we know for sure it's supported!
active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value)
self._discovered_devices[key].connect()
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
else:
self._onDeviceConnectionStateChanged(key)
else:
if self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to close connection with [%s]" % key)
self._discovered_devices[key].close()
self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
## Start the network discovery.
def start(self):
# 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.
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.
self._manual_instances = self._getStoredManualInstances()
for address in self._manual_instances:
if address:
self.addManualDevice(address)
# TODO: self.resetLastManualDevice()
## Stop network discovery and clean up discovered devices.
def stop(self):
# Cleanup ZeroConf resources.
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.
for instance_name in list(self._discovered_devices):
self._onRemoveDevice(instance_name)
## Add a networked printer manually by address.
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
self._manual_instances[address] = ManualPrinterRequest(address, callback=callback)
new_manual_devices = ",".join(self._manual_instances.keys())
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices)
key = f"manual:{address}"
if key not in self._discovered_devices:
self._onAddDevice(key, address, {
b"name": address.encode("utf-8"),
b"address": address.encode("utf-8"),
b"manual": b"true",
b"incomplete": b"true",
b"temporary": b"true"
})
self._last_manual_entry_key = key
response_callback = lambda status_code, response: self._onCheckManualDeviceResponse(status_code, address)
self._checkManualDevice(address, response_callback)
## Remove a manually added networked printer.
def removeManualDevice(self, key: str, address: Optional[str] = None) -> None:
if key not in self._discovered_devices and address is not None:
key = f"manual:{address}"
if key in self._discovered_devices:
if not address:
address = self._discovered_devices[key].ipAddress
self._onRemoveDevice(key)
# TODO: self.resetLastManualDevice()
if address in self._manual_instances:
manual_printer_request = self._manual_instances.pop(address)
new_manual_devices = ",".join(self._manual_instances.keys())
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY,
new_manual_devices)
if manual_printer_request.callback is not None:
CuraApplication.getInstance().callLater(manual_printer_request.callback, False, address)
## Checks if a networked printer exists at the given address.
# If the printer responds it will replace the preliminary printer created from the stored manual instances.
def _checkManualDevice(self, address: str, on_finished: Callable) -> None:
Logger.log("d", "checking manual device: {}".format(address))
url = QUrl(f"http://{address}/{self.PRINTER_API_PREFIX}/system")
request = QNetworkRequest(url)
reply = self._network_manager.get(request)
self._addCallback(reply, on_finished)
## Callback for when a manual device check request was responded to.
def _onCheckManualDeviceResponse(self, status_code: int, address: str) -> None:
Logger.log("d", "manual device check response: {} {}".format(status_code, address))
if address in self._manual_instances:
callback = self._manual_instances[address].callback
if callback:
CuraApplication.getInstance().callLater(callback, status_code == 200, address)
## Returns a dict of printer BOM numbers to machine types.
# These numbers are available in the machine definition already so we just search for them here.
@staticmethod
def _getPrinterTypeIdentifiers() -> Dict[str, str]:
container_registry = CuraApplication.getInstance().getContainerRegistry()
ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.")
found_machine_type_identifiers = {} # type: Dict[str, str]
for machine in ultimaker_machines:
machine_bom_number = machine.get("firmware_update_info", {}).get("id", None)
machine_type = machine.get("id", None)
if machine_bom_number and machine_type:
found_machine_type_identifiers[str(machine_bom_number)] = machine_type
return found_machine_type_identifiers
## Add a new device.
def _onAddDevice(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None:
cluster_size = int(properties.get(b"cluster_size", -1))
printer_type = properties.get(b"machine", b"").decode("utf-8")
printer_type_identifiers = self._getPrinterTypeIdentifiers()
# Detect the machine type based on the BOM number that is sent over the network.
for bom, p_type in printer_type_identifiers.items():
if printer_type.startswith(bom):
properties[b"printer_type"] = bytes(p_type, encoding="utf8")
break
else:
properties[b"printer_type"] = b"Unknown"
# We no longer support legacy devices, so check that here.
if cluster_size == -1:
return
device = ClusterUM3OutputDevice(key, address, properties)
CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter(
ip_address=address,
key=device.getId(),
name=properties[b"name"].decode("utf-8"),
create_callback=self._createMachineFromDiscoveredPrinter,
machine_type=properties[b"printer_type"].decode("utf-8"),
device=device
)
self._discovered_devices[device.getId()] = device
self.discoveredDevicesChanged.emit()
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
# Ensure that the configured connection type is set.
global_container_stack.addConfiguredConnectionType(device.connectionType.value)
device.connect()
device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
## Remove a device.
def _onRemoveDevice(self, device_id: str) -> None:
device = self._discovered_devices.pop(device_id, None)
if device:
if device.isConnected():
device.disconnect()
try:
device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
except TypeError:
# Disconnect already happened.
pass
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address)
self.discoveredDevicesChanged.emit()
## 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)
## Callback handler for when the connection state of a networked device has changed.
def _onDeviceConnectionStateChanged(self, key: str) -> None:
if key not in self._discovered_devices:
return
if self._discovered_devices[key].isConnected():
um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
if key != um_network_key:
return
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
# TODO: self.checkCloudFlowIsPossible(None)
else:
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(key)
## 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", "Bonjour service removed: %s" % name)
self.removedNetworkCluster.emit(str(name))
return True
def _associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None:
if not printer_device:
return
Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key)
machine_manager = CuraApplication.getInstance().getMachineManager()
global_container_stack = machine_manager.activeMachine
if not global_container_stack:
return
for machine in machine_manager.getMachinesInGroup(global_container_stack.getMetaDataEntry("group_id")):
machine.setMetaDataEntry("um_network_key", printer_device.key)
machine.setMetaDataEntry("group_name", printer_device.name)
# Delete old authentication data.
Logger.log("d", "Removing old authentication id %s for device %s",
global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key)
machine.removeMetaDataEntry("network_authentication_id")
machine.removeMetaDataEntry("network_authentication_key")
# Ensure that these containers do know that they are configured for network connection
machine.addConfiguredConnectionType(printer_device.connectionType.value)
self.refreshConnections()
## Create a machine instance based on the discovered network printer.
def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
discovered_device = self._discovered_devices.get(key)
if discovered_device is None:
Logger.log("e", "Could not find discovered device with key [%s]", key)
return
group_name = discovered_device.getProperty("name")
machine_type_id = discovered_device.getProperty("printer_type")
Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]",
key, group_name, machine_type_id)
CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name)
# connect the new machine to that network printer
self._associateActiveMachineWithPrinterDevice(discovered_device)
# ensure that the connection states are refreshed.
self.refreshConnections()
## Creates a callback function so that it includes the parsing of the response into the correct model.
# The callback is added to the 'finished' signal of the reply.
# \param reply: The reply that should be listened to.
# \param on_finished: The callback in case the response is successful.
def _addCallback(self, reply: QNetworkReply, on_finished: Callable) -> None:
def parse() -> None:
# Don't try to parse the reply if we didn't get one
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
return
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
response = bytes(reply.readAll()).decode()
self._anti_gc_callbacks.remove(parse)
on_finished(int(status_code), response)
return
self._anti_gc_callbacks.append(parse)
reply.finished.connect(parse)
## Load the user-configured manual devices from Cura preferences.
def _getStoredManualInstances(self) -> Dict[str, ManualPrinterRequest]:
preferences = CuraApplication.getInstance().getPreferences()
preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "")
manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",")
return {address: ManualPrinterRequest(address) for address in manual_instances}