Convert doxygen to rst for UM3NetworkPrinting

This commit is contained in:
Nino van Hooff 2020-05-15 15:05:38 +02:00
parent de82406782
commit 5eb5ffd916
38 changed files with 797 additions and 487 deletions

View file

@ -16,12 +16,13 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus
from ..Models.Http.ClusterMaterial import ClusterMaterial
## The generic type variable used to document the methods below.
ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel)
"""The generic type variable used to document the methods below."""
## The ClusterApiClient is responsible for all network calls to local network clusters.
class ClusterApiClient:
"""The ClusterApiClient is responsible for all network calls to local network clusters."""
PRINTER_API_PREFIX = "/api/v1"
CLUSTER_API_PREFIX = "/cluster-api/v1"
@ -29,75 +30,92 @@ class ClusterApiClient:
# In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[], None]]
## Initializes a new cluster API client.
# \param address: The network address of the cluster to call.
# \param on_error: The callback to be called whenever we receive errors from the server.
def __init__(self, address: str, on_error: Callable) -> None:
"""Initializes a new cluster API client.
:param address: The network address of the cluster to call.
:param on_error: The callback to be called whenever we receive errors from the server.
"""
super().__init__()
self._manager = QNetworkAccessManager()
self._address = address
self._on_error = on_error
## Get printer system information.
# \param on_finished: The callback in case the response is successful.
def getSystem(self, on_finished: Callable) -> None:
"""Get printer system information.
:param on_finished: The callback in case the response is successful.
"""
url = "{}/system".format(self.PRINTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, PrinterSystemStatus)
## Get the installed materials on the printer.
# \param on_finished: The callback in case the response is successful.
def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None:
"""Get the installed materials on the printer.
:param on_finished: The callback in case the response is successful.
"""
url = "{}/materials".format(self.CLUSTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, ClusterMaterial)
## Get the printers in the cluster.
# \param on_finished: The callback in case the response is successful.
def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None:
"""Get the printers in the cluster.
:param on_finished: The callback in case the response is successful.
"""
url = "{}/printers".format(self.CLUSTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, ClusterPrinterStatus)
## Get the print jobs in the cluster.
# \param on_finished: The callback in case the response is successful.
def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None:
"""Get the print jobs in the cluster.
:param on_finished: The callback in case the response is successful.
"""
url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX)
reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished, ClusterPrintJobStatus)
## Move a print job to the top of the queue.
def movePrintJobToTop(self, print_job_uuid: str) -> None:
"""Move a print job to the top of the queue."""
url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode())
## Override print job configuration and force it to be printed.
def forcePrintJob(self, print_job_uuid: str) -> None:
"""Override print job configuration and force it to be printed."""
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode())
## Delete a print job from the queue.
def deletePrintJob(self, print_job_uuid: str) -> None:
"""Delete a print job from the queue."""
url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid)
self._manager.deleteResource(self._createEmptyRequest(url))
## Set the state of a print job.
def setPrintJobState(self, print_job_uuid: str, state: str) -> None:
"""Set the state of a print job."""
url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid)
# We rewrite 'resume' to 'print' here because we are using the old print job action endpoints.
action = "print" if state == "resume" else state
self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode())
## Get the preview image data of a print job.
def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None:
"""Get the preview image data of a print job."""
url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid)
reply = self._manager.get(self._createEmptyRequest(url))
self._addCallback(reply, on_finished)
## We override _createEmptyRequest in order to add the user credentials.
# \param url: The URL to request
# \param content_type: The type of the body contents.
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
"""We override _createEmptyRequest in order to add the user credentials.
:param url: The URL to request
:param content_type: The type of the body contents.
"""
url = QUrl("http://" + self._address + path)
request = QNetworkRequest(url)
request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
@ -105,11 +123,13 @@ class ClusterApiClient:
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
return request
## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
# \param reply: The reply from the server.
# \return A tuple with a status code and a dictionary.
@staticmethod
def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]:
"""Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
:param reply: The reply from the server.
:return: A tuple with a status code and a dictionary.
"""
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
try:
response = bytes(reply.readAll()).decode()
@ -118,14 +138,15 @@ class ClusterApiClient:
Logger.logException("e", "Could not parse the cluster response: %s", err)
return status_code, {"errors": [err]}
## Parses the given models and calls the correct callback depending on the result.
# \param response: The response from the server, after being converted to a dict.
# \param on_finished: The callback in case the response is successful.
# \param model_class: The type of the model to convert the response to. It may either be a single record or a list.
def _parseModels(self, response: Dict[str, Any],
on_finished: Union[Callable[[ClusterApiClientModel], Any],
Callable[[List[ClusterApiClientModel]], Any]],
model_class: Type[ClusterApiClientModel]) -> None:
def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any],
Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None:
"""Parses the given models and calls the correct callback depending on the result.
:param response: The response from the server, after being converted to a dict.
:param on_finished: The callback in case the response is successful.
:param model_class: The type of the model to convert the response to. It may either be a single record or a list.
"""
try:
if isinstance(response, list):
results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel]
@ -138,16 +159,15 @@ class ClusterApiClient:
except (JSONDecodeError, TypeError, ValueError):
Logger.log("e", "Could not parse response from network: %s", str(response))
## 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: Union[Callable[[ClusterApiClientModel], Any],
Callable[[List[ClusterApiClientModel]], Any]],
model: Type[ClusterApiClientModel] = None,
def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any],
Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None,
) -> None:
"""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 parse() -> None:
self._anti_gc_callbacks.remove(parse)

View file

@ -51,15 +51,17 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._setInterfaceElements()
self._active_camera_url = QUrl() # type: QUrl
## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None:
"""Set all the interface elements and texts for this output device."""
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setShortDescription(I18N_CATALOG.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network"))
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network"))
## Called when the connection to the cluster changes.
def connect(self) -> None:
"""Called when the connection to the cluster changes."""
super().connect()
self._update()
self.sendMaterialProfiles()
@ -94,10 +96,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
def forceSendJob(self, print_job_uuid: str) -> None:
self._getApiClient().forcePrintJob(print_job_uuid)
## Set the remote print job state.
# \param print_job_uuid: The UUID of the print job to set the state for.
# \param action: The action to undertake ('pause', 'resume', 'abort').
def setJobState(self, print_job_uuid: str, action: str) -> None:
"""Set the remote print job state.
:param print_job_uuid: The UUID of the print job to set the state for.
:param action: The action to undertake ('pause', 'resume', 'abort').
"""
self._getApiClient().setPrintJobState(print_job_uuid, action)
def _update(self) -> None:
@ -106,19 +111,22 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._getApiClient().getPrintJobs(self._updatePrintJobs)
self._updatePrintJobPreviewImages()
## Get a list of materials that are installed on the cluster host.
def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None:
"""Get a list of materials that are installed on the cluster host."""
self._getApiClient().getMaterials(on_finished = on_finished)
## Sync the material profiles in Cura with the printer.
# This gets called when connecting to a printer as well as when sending a print.
def sendMaterialProfiles(self) -> None:
"""Sync the material profiles in Cura with the printer.
This gets called when connecting to a printer as well as when sending a print.
"""
job = SendMaterialJob(device = self)
job.run()
## Send a print job to the cluster.
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
"""Send a print job to the cluster."""
# Show an error message if we're already sending a job.
if self._progress.visible:
@ -132,15 +140,20 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
job.finished.connect(self._onPrintJobCreated)
job.start()
## Allows the user to choose a printer to print with from the printer selection dialogue.
# \param unique_name: The unique name of the printer to target.
@pyqtSlot(str, name="selectTargetPrinter")
def selectTargetPrinter(self, unique_name: str = "") -> None:
"""Allows the user to choose a printer to print with from the printer selection dialogue.
:param unique_name: The unique name of the printer to target.
"""
self._startPrintJobUpload(unique_name if unique_name != "" else None)
## Handler for when the print job was created locally.
# It can now be sent over the network.
def _onPrintJobCreated(self, job: ExportFileJob) -> None:
"""Handler for when the print job was created locally.
It can now be sent over the network.
"""
self._active_exported_job = job
# TODO: add preference to enable/disable this feature?
if self.clusterSize > 1:
@ -148,8 +161,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return
self._startPrintJobUpload()
## Shows a dialog allowing the user to select which printer in a group to send a job to.
def _showPrinterSelectionDialog(self) -> None:
"""Shows a dialog allowing the user to select which printer in a group to send a job to."""
if not self._printer_select_dialog:
plugin_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or ""
path = os.path.join(plugin_path, "resources", "qml", "PrintWindow.qml")
@ -157,8 +171,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
if self._printer_select_dialog is not None:
self._printer_select_dialog.show()
## Upload the print job to the group.
def _startPrintJobUpload(self, unique_name: str = None) -> None:
"""Upload the print job to the group."""
if not self._active_exported_job:
Logger.log("e", "No active exported job to upload!")
return
@ -177,33 +192,40 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
on_progress=self._onPrintJobUploadProgress)
self._active_exported_job = None
## Handler for print job upload progress.
def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None:
"""Handler for print job upload progress."""
percentage = (bytes_sent / bytes_total) if bytes_total else 0
self._progress.setProgress(percentage * 100)
self.writeProgress.emit()
## Handler for when the print job was fully uploaded to the cluster.
def _onPrintUploadCompleted(self, _: QNetworkReply) -> None:
"""Handler for when the print job was fully uploaded to the cluster."""
self._progress.hide()
PrintJobUploadSuccessMessage().show()
self.writeFinished.emit()
## Displays the given message if uploading the mesh has failed
# \param message: The message to display.
def _onUploadError(self, message: str = None) -> None:
"""Displays the given message if uploading the mesh has failed
:param message: The message to display.
"""
self._progress.hide()
PrintJobUploadErrorMessage(message).show()
self.writeError.emit()
## Download all the images from the cluster and load their data in the print job models.
def _updatePrintJobPreviewImages(self):
"""Download all the images from the cluster and load their data in the print job models."""
for print_job in self._print_jobs:
if print_job.getPreviewImage() is None:
self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData)
## Get the API client instance.
def _getApiClient(self) -> ClusterApiClient:
"""Get the API client instance."""
if not self._cluster_api:
self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error)))
return self._cluster_api

View file

@ -24,8 +24,9 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus
I18N_CATALOG = i18nCatalog("cura")
## The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters.
class LocalClusterOutputDeviceManager:
"""The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters."""
META_NETWORK_KEY = "um_network_key"
@ -49,30 +50,35 @@ class LocalClusterOutputDeviceManager:
self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered)
self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved)
## Start the network discovery.
def start(self) -> None:
"""Start the network discovery."""
self._zero_conf_client.start()
for address in self._getStoredManualAddresses():
self.addManualDevice(address)
## Stop network discovery and clean up discovered devices.
def stop(self) -> None:
"""Stop network discovery and clean up discovered devices."""
self._zero_conf_client.stop()
for instance_name in list(self._discovered_devices):
self._onDiscoveredDeviceRemoved(instance_name)
## Restart discovery on the local network.
def startDiscovery(self):
"""Restart discovery on the local network."""
self.stop()
self.start()
## Add a networked printer manually by address.
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
"""Add a networked printer manually by address."""
api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error)))
api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback))
## Remove a manually added networked printer.
def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None:
"""Remove a manually added networked printer."""
if device_id not in self._discovered_devices and address is not None:
device_id = "manual:{}".format(address)
@ -83,16 +89,19 @@ class LocalClusterOutputDeviceManager:
if address in self._getStoredManualAddresses():
self._removeStoredManualAddress(address)
## Force reset all network device connections.
def refreshConnections(self) -> None:
"""Force reset all network device connections."""
self._connectToActiveMachine()
## Get the discovered devices.
def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]:
"""Get the discovered devices."""
return self._discovered_devices
## Connect the active machine to a given device.
def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None:
"""Connect the active machine to a given device."""
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine:
return
@ -106,8 +115,9 @@ class LocalClusterOutputDeviceManager:
return
CuraApplication.getInstance().getMachineManager().switchPrinterType(definitions[0].getName())
## Callback for when the active machine was changed by the user or a new remote cluster was found.
def _connectToActiveMachine(self) -> None:
"""Callback for when the active machine was changed by the user or a new remote cluster was found."""
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if not active_machine:
return
@ -122,9 +132,10 @@ class LocalClusterOutputDeviceManager:
# Remove device if it is not meant for the active machine.
output_device_manager.removeOutputDevice(device.key)
## Callback for when a manual device check request was responded to.
def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus,
callback: Optional[Callable[[bool, str], None]] = None) -> None:
"""Callback for when a manual device check request was responded to."""
self._onDeviceDiscovered("manual:{}".format(address), address, {
b"name": status.name.encode("utf-8"),
b"address": address.encode("utf-8"),
@ -137,10 +148,13 @@ class LocalClusterOutputDeviceManager:
if callback is not None:
CuraApplication.getInstance().callLater(callback, True, 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]:
"""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.
"""
container_registry = CuraApplication.getInstance().getContainerRegistry()
ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.")
found_machine_type_identifiers = {} # type: Dict[str, str]
@ -154,8 +168,9 @@ class LocalClusterOutputDeviceManager:
found_machine_type_identifiers[str(bom_number)] = machine_type
return found_machine_type_identifiers
## Add a new device.
def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None:
"""Add a new device."""
machine_identifier = properties.get(b"machine", b"").decode("utf-8")
printer_type_identifiers = self._getPrinterTypeIdentifiers()
@ -189,8 +204,9 @@ class LocalClusterOutputDeviceManager:
self.discoveredDevicesChanged.emit()
self._connectToActiveMachine()
## Remove a device.
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
"""Remove a device."""
device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice]
if not device:
return
@ -198,8 +214,9 @@ class LocalClusterOutputDeviceManager:
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address)
self.discoveredDevicesChanged.emit()
## Create a machine instance based on the discovered network printer.
def _createMachineFromDiscoveredDevice(self, device_id: str) -> None:
"""Create a machine instance based on the discovered network printer."""
device = self._discovered_devices.get(device_id)
if device is None:
return
@ -216,8 +233,9 @@ class LocalClusterOutputDeviceManager:
self._connectToOutputDevice(device, new_machine)
self._showCloudFlowMessage(device)
## Add an address to the stored preferences.
def _storeManualAddress(self, address: str) -> None:
"""Add an address to the stored preferences."""
stored_addresses = self._getStoredManualAddresses()
if address in stored_addresses:
return # Prevent duplicates.
@ -225,8 +243,9 @@ class LocalClusterOutputDeviceManager:
new_value = ",".join(stored_addresses)
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value)
## Remove an address from the stored preferences.
def _removeStoredManualAddress(self, address: str) -> None:
"""Remove an address from the stored preferences."""
stored_addresses = self._getStoredManualAddresses()
try:
stored_addresses.remove(address) # Can throw a ValueError
@ -235,15 +254,16 @@ class LocalClusterOutputDeviceManager:
except ValueError:
Logger.log("w", "Could not remove address from stored_addresses, it was not there")
## Load the user-configured manual devices from Cura preferences.
def _getStoredManualAddresses(self) -> List[str]:
"""Load the user-configured manual devices from Cura preferences."""
preferences = CuraApplication.getInstance().getPreferences()
preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "")
manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",")
return manual_instances
## Add a device to the current active machine.
def _connectToOutputDevice(self, device: UltimakerNetworkedPrinterOutputDevice, machine: GlobalStack) -> None:
"""Add a device to the current active machine."""
# Make sure users know that we no longer support legacy devices.
if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION:
@ -262,9 +282,10 @@ class LocalClusterOutputDeviceManager:
if device.key not in output_device_manager.getOutputDeviceIds():
output_device_manager.addOutputDevice(device)
## Nudge the user to start using Ultimaker Cloud.
@staticmethod
def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None:
"""Nudge the user to start using Ultimaker Cloud."""
if CuraApplication.getInstance().getMachineManager().activeMachineIsUsingCloudConnection:
# This printer is already cloud connected, so we do not bother the user anymore.
return

View file

@ -16,27 +16,33 @@ if TYPE_CHECKING:
from .LocalClusterOutputDevice import LocalClusterOutputDevice
## Asynchronous job to send material profiles to the printer.
#
# This way it won't freeze up the interface while sending those materials.
class SendMaterialJob(Job):
"""Asynchronous job to send material profiles to the printer.
This way it won't freeze up the interface while sending those materials.
"""
def __init__(self, device: "LocalClusterOutputDevice") -> None:
super().__init__()
self.device = device # type: LocalClusterOutputDevice
## Send the request to the printer and register a callback
def run(self) -> None:
"""Send the request to the printer and register a callback"""
self.device.getMaterials(on_finished = self._onGetMaterials)
## Callback for when the remote materials were returned.
def _onGetMaterials(self, materials: List[ClusterMaterial]) -> None:
"""Callback for when the remote materials were returned."""
remote_materials_by_guid = {material.guid: material for material in materials}
self._sendMissingMaterials(remote_materials_by_guid)
## Determine which materials should be updated and send them to the printer.
# \param remote_materials_by_guid The remote materials by GUID.
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
"""Determine which materials should be updated and send them to the printer.
:param remote_materials_by_guid: The remote materials by GUID.
"""
local_materials_by_guid = self._getLocalMaterials()
if len(local_materials_by_guid) == 0:
Logger.log("d", "There are no local materials to synchronize with the printer.")
@ -47,25 +53,31 @@ class SendMaterialJob(Job):
return
self._sendMaterials(material_ids_to_send)
## From the local and remote materials, determine which ones should be synchronized.
# Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that
# are newer in Cura.
# \param local_materials The local materials by GUID.
# \param remote_materials The remote materials by GUID.
@staticmethod
def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial],
remote_materials: Dict[str, ClusterMaterial]) -> Set[str]:
"""From the local and remote materials, determine which ones should be synchronized.
Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that
are newer in Cura.
:param local_materials: The local materials by GUID.
:param remote_materials: The remote materials by GUID.
"""
return {
local_material.id
for guid, local_material in local_materials.items()
if guid not in remote_materials.keys() or local_material.version > remote_materials[guid].version
}
## Send the materials to the printer.
# The given materials will be loaded from disk en sent to to printer.
# The given id's will be matched with filenames of the locally stored materials.
# \param materials_to_send A set with id's of materials that must be sent.
def _sendMaterials(self, materials_to_send: Set[str]) -> None:
"""Send the materials to the printer.
The given materials will be loaded from disk en sent to to printer.
The given id's will be matched with filenames of the locally stored materials.
:param materials_to_send: A set with id's of materials that must be sent.
"""
container_registry = CuraApplication.getInstance().getContainerRegistry()
all_materials = container_registry.findInstanceContainersMetadata(type = "material")
all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material).
@ -83,12 +95,14 @@ class SendMaterialJob(Job):
file_name = os.path.basename(file_path)
self._sendMaterialFile(file_path, file_name, root_material_id)
## Send a single material file to the printer.
# Also add the material signature file if that is available.
# \param file_path The path of the material file.
# \param file_name The name of the material file.
# \param material_id The ID of the material in the file.
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
"""Send a single material file to the printer.
Also add the material signature file if that is available.
:param file_path: The path of the material file.
:param file_name: The name of the material file.
:param material_id: The ID of the material in the file.
"""
parts = []
# Add the material file.
@ -112,8 +126,9 @@ class SendMaterialJob(Job):
self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts,
on_finished = self._sendingFinished)
## Check a reply from an upload to the printer and log an error when the call failed
def _sendingFinished(self, reply: QNetworkReply) -> None:
"""Check a reply from an upload to the printer and log an error when the call failed"""
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("w", "Error while syncing material: %s", reply.errorString())
return
@ -125,11 +140,14 @@ class SendMaterialJob(Job):
# Because of the guards above it is not shown when syncing failed (which is not always an actual problem).
MaterialSyncMessage(self.device).show()
## Retrieves a list of local materials
# Only the new newest version of the local materials is returned
# \return a dictionary of LocalMaterial objects by GUID
@staticmethod
def _getLocalMaterials() -> Dict[str, LocalMaterial]:
"""Retrieves a list of local materials
Only the new newest version of the local materials is returned
:return: a dictionary of LocalMaterial objects by GUID
"""
result = {} # type: Dict[str, LocalMaterial]
all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material")
all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent.

View file

@ -12,9 +12,11 @@ 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 ZeroConfClient handles all network discovery logic.
It emits signals when new network services were found or disappeared.
"""
# The discovery protocol name for Ultimaker printers.
ZERO_CONF_NAME = u"_ultimaker._tcp.local."
@ -30,10 +32,13 @@ class ZeroConfClient:
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:
"""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()
try:
@ -56,16 +61,18 @@ class ZeroConfClient:
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:
"""Handles a change is discovered network services."""
item = (zeroconf, service_type, name, state_change)
if not self._service_changed_request_queue or not self._service_changed_request_event:
return
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:
"""Callback for when a ZeroConf service has changes."""
if not self._service_changed_request_queue or not self._service_changed_request_event:
return
@ -98,19 +105,23 @@ class ZeroConfClient:
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:
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str,
state_change: ServiceStateChange) -> bool:
"""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.
"""
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:
"""Handler for when a ZeroConf service was added."""
# 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()):
@ -141,8 +152,9 @@ class ZeroConfClient:
return True
## Handler for when a ZeroConf service was removed.
def _onServiceRemoved(self, name: str) -> bool:
"""Handler for when a ZeroConf service was removed."""
Logger.log("d", "ZeroConf service removed: %s" % name)
self.removedNetworkCluster.emit(str(name))
return True