mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-25 15:44:04 -06:00
Convert doxygen to rst for UM3NetworkPrinting
This commit is contained in:
parent
de82406782
commit
5eb5ffd916
38 changed files with 797 additions and 487 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue