mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-13 17:57:55 -06:00
241 lines
10 KiB
Python
241 lines
10 KiB
Python
# Copyright (c) 2020 Ultimaker B.V.
|
|
# Cura is released under the terms of the LGPLv3 or higher.
|
|
import os
|
|
from typing import Optional, Dict, List, Callable, Any
|
|
|
|
from time import time
|
|
|
|
from PyQt6.QtGui import QDesktopServices
|
|
from PyQt6.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
|
from PyQt6.QtNetwork import QNetworkReply
|
|
|
|
from UM.FileHandler.FileHandler import FileHandler
|
|
from UM.Version import Version
|
|
from UM.i18n import i18nCatalog
|
|
from UM.Logger import Logger
|
|
from UM.Scene.SceneNode import SceneNode
|
|
from cura.CuraApplication import CuraApplication
|
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
|
|
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
|
|
|
from .ClusterApiClient import ClusterApiClient
|
|
from .SendMaterialJob import SendMaterialJob
|
|
from ..ExportFileJob import ExportFileJob
|
|
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
|
|
from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage
|
|
from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage
|
|
from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage
|
|
from ..Models.Http.ClusterMaterial import ClusterMaterial
|
|
|
|
|
|
I18N_CATALOG = i18nCatalog("cura")
|
|
|
|
|
|
class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|
|
|
activeCameraUrlChanged = pyqtSignal()
|
|
|
|
CHECK_CLUSTER_INTERVAL = 10.0 # seconds
|
|
|
|
def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], parent=None) -> None:
|
|
|
|
super().__init__(
|
|
device_id=device_id,
|
|
address=address,
|
|
properties=properties,
|
|
connection_type=ConnectionType.NetworkConnection,
|
|
parent=parent
|
|
)
|
|
self._timeout_time = 30
|
|
self._cluster_api = None # type: Optional[ClusterApiClient]
|
|
self._active_exported_job = None # type: Optional[ExportFileJob]
|
|
self._printer_select_dialog = None # type: Optional[QObject]
|
|
|
|
# We don't have authentication over local networking, so we're always authenticated.
|
|
self.setAuthenticationState(AuthState.Authenticated)
|
|
self._setInterfaceElements()
|
|
self._active_camera_url = QUrl() # type: QUrl
|
|
|
|
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"))
|
|
|
|
def connect(self) -> None:
|
|
"""Called when the connection to the cluster changes."""
|
|
|
|
super().connect()
|
|
self._update()
|
|
self.sendMaterialProfiles()
|
|
|
|
@pyqtProperty(QUrl, notify=activeCameraUrlChanged)
|
|
def activeCameraUrl(self) -> QUrl:
|
|
return self._active_camera_url
|
|
|
|
@pyqtSlot(QUrl, name="setActiveCameraUrl")
|
|
def setActiveCameraUrl(self, camera_url: QUrl) -> None:
|
|
if self._active_camera_url != camera_url:
|
|
self._active_camera_url = camera_url
|
|
self.activeCameraUrlChanged.emit()
|
|
|
|
@pyqtSlot(name="openPrintJobControlPanel")
|
|
def openPrintJobControlPanel(self) -> None:
|
|
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
|
|
|
@pyqtSlot(name="openPrinterControlPanel")
|
|
def openPrinterControlPanel(self) -> None:
|
|
if Version(self.firmwareVersion) >= Version("7.0.2"):
|
|
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
|
else:
|
|
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
|
|
|
@pyqtSlot(str, name="sendJobToTop")
|
|
def sendJobToTop(self, print_job_uuid: str) -> None:
|
|
self._getApiClient().movePrintJobToTop(print_job_uuid)
|
|
|
|
@pyqtSlot(str, name="deleteJobFromQueue")
|
|
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
|
|
self._getApiClient().deletePrintJob(print_job_uuid)
|
|
|
|
@pyqtSlot(str, name="forceSendJob")
|
|
def forceSendJob(self, print_job_uuid: str) -> None:
|
|
self._getApiClient().forcePrintJob(print_job_uuid)
|
|
|
|
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:
|
|
super()._update()
|
|
if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
|
|
return # avoid calling the cluster too often
|
|
self._getApiClient().getPrinters(self._updatePrinters)
|
|
self._getApiClient().getPrintJobs(self._updatePrintJobs)
|
|
self._updatePrintJobPreviewImages()
|
|
|
|
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)
|
|
|
|
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()
|
|
|
|
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:
|
|
PrintJobUploadBlockedMessage().show()
|
|
return
|
|
|
|
self.writeStarted.emit(self)
|
|
|
|
# Export the scene to the correct file type.
|
|
job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion)
|
|
job.finished.connect(self._onPrintJobCreated)
|
|
job.start()
|
|
|
|
@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)
|
|
|
|
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:
|
|
self._showPrinterSelectionDialog() # self._startPrintJobUpload will be triggered from this dialog
|
|
return
|
|
self._startPrintJobUpload()
|
|
|
|
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")
|
|
self._printer_select_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
|
|
if self._printer_select_dialog is not None:
|
|
self._printer_select_dialog.show()
|
|
|
|
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
|
|
self._progress.show()
|
|
parts = [
|
|
self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"),
|
|
self._createFormPart("name=\"file\"; filename=\"%s\"" % self._active_exported_job.getFileName(),
|
|
self._active_exported_job.getOutput())
|
|
]
|
|
# If a specific printer was selected we include the name in the request.
|
|
# FIXME: Connect should allow the printer UUID here instead of the 'unique_name'.
|
|
if unique_name is not None:
|
|
parts.append(self._createFormPart("name=require_printer_name", bytes(unique_name, "utf-8"), "text/plain"))
|
|
# FIXME: move form posting to API client
|
|
self.postFormWithParts("/cluster-api/v1/print_jobs/", parts, on_finished=self._onPrintUploadCompleted,
|
|
on_progress=self._onPrintJobUploadProgress)
|
|
self._active_exported_job = None
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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)
|
|
|
|
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
|