re-implement requestWrite

This commit is contained in:
ChrisTerBeke 2019-07-29 23:12:55 +02:00
parent 529b483f36
commit b90e5b3262
4 changed files with 92 additions and 262 deletions

View file

@ -17,7 +17,6 @@ from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from .CloudProgressMessage import CloudProgressMessage
from .CloudApiClient import CloudApiClient
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
from ..MeshFormatHandler import MeshFormatHandler
@ -84,15 +83,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._account = api_client.account
self._cluster = cluster
self.setAuthenticationState(AuthState.NotAuthenticated)
self._setInterfaceElements()
# Trigger the printersChanged signal when the private signal is triggered.
self.printersChanged.connect(self._clusterPrintersChanged)
# We only allow a single upload at a time.
self._progress = CloudProgressMessage()
# Keep server string of the last generated time to avoid updating models more than once for the same response
self._received_printers = None # type: Optional[List[ClusterPrinterStatus]]
self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]]
@ -143,15 +138,14 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
## Called when Cura requests an output device to receive a (G-code) file.
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:
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:
# Show an error message if we're already sending a job.
if self._progress.visible:
message = Message(
text=I18N_CATALOG.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."),
title=I18N_CATALOG.i18nc("@info:title", "Cloud error"),
text=I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."),
title=I18N_CATALOG.i18nc("@info:title", "Print error"),
lifetime=10
)
message.show()
@ -170,8 +164,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
Logger.log("e", "Missing file or mesh writer!")
return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job."))
# TODO: use stream just like the network output device
mesh = mesh_format.getBytes(nodes)
self._tool_path = mesh
request = CloudPrintJobUploadRequest(
job_name=file_name or mesh_format.file_extension,
@ -236,7 +230,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
## Shows a message when the upload has succeeded
# \param response: The response from the cloud API.
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
Logger.log("d", "The cluster will be printing this print job with the ID %s", response.cluster_job_id)
self._progress.hide()
Message(
text=I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),

View file

@ -6,13 +6,17 @@ from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty
from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.WriteFileJob import WriteFileJob
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
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 ..MeshFormatHandler import MeshFormatHandler
from ..SendMaterialJob import SendMaterialJob
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
@ -38,22 +42,9 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._cluster_api = ClusterApiClient(address, on_error=self._onNetworkError)
# 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
# self._number_of_extruders = 2
# self._dummy_lambdas = (
# "", {}, io.BytesIO()
# ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]]
# self._error_message = None # type: Optional[Message]
# self._write_job_progress_message = None # type: Optional[Message]
# self._progress_message = None # type: Optional[Message]
# self._printer_selection_dialog = None # type: QObject
# self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str]
# self._finished_jobs = [] # type: List[UM3PrintJobOutputModel]
# self._sending_job = None
## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None:
@ -123,189 +114,7 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
job = SendMaterialJob(device=self)
job.run()
# TODO FROM HERE
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:
pass
# self.writeStarted.emit(self)
#
# self.sendMaterialProfiles()
#
# mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
#
# # This function pauses with the yield, waiting on instructions on which printer it needs to print with.
# if not mesh_format.is_valid:
# Logger.log("e", "Missing file or mesh writer!")
# return
# self._sending_job = self._sendPrintJob(mesh_format, nodes)
# if self._sending_job is not None:
# self._sending_job.send(None) # Start the generator.
#
# if len(self._printers) > 1: # We need to ask the user.
# self._spawnPrinterSelectionDialog()
# else: # Just immediately continue.
# self._sending_job.send("") # No specifically selected printer.
# self._sending_job.send(None)
#
# def _spawnPrinterSelectionDialog(self):
# if self._printer_selection_dialog is None:
# if PluginRegistry.getInstance() is not None:
# path = os.path.join(
# PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
# "resources", "qml", "PrintWindow.qml"
# )
# self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self})
# if self._printer_selection_dialog is not None:
# self._printer_selection_dialog.show()
# ## Allows the user to choose a printer to print with from the printer
# # selection dialogue.
# # \param target_printer The name of the printer to target.
# @pyqtSlot(str)
# def selectPrinter(self, target_printer: str = "") -> None:
# if self._sending_job is not None:
# self._sending_job.send(target_printer)
# @pyqtSlot()
# def cancelPrintSelection(self) -> None:
# self._sending_gcode = False
# ## Greenlet to send a job to the printer over the network.
# #
# # This greenlet gets called asynchronously in requestWrite. It is a
# # greenlet in order to optionally wait for selectPrinter() to select a
# # printer.
# # The greenlet yields exactly three times: First time None,
# # \param mesh_format Object responsible for choosing the right kind of format to write with.
# def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]):
# Logger.log("i", "Sending print job to printer.")
# if self._sending_gcode:
# self._error_message = Message(
# I18N_CATALOG.i18nc("@info:status",
# "Sending new jobs (temporarily) blocked, still sending the previous print job."))
# self._error_message.show()
# yield #Wait on the user to select a target printer.
# yield #Wait for the write job to be finished.
# yield False #Return whether this was a success or not.
# yield #Prevent StopIteration.
#
# self._sending_gcode = True
#
# # Potentially wait on the user to select a target printer.
# target_printer = yield # type: Optional[str]
#
# # Using buffering greatly reduces the write time for many lines of gcode
#
# stream = mesh_format.createStream()
#
# job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode)
#
# self._write_job_progress_message = Message(I18N_CATALOG.i18nc("@info:status", "Sending data to printer"),
# lifetime = 0, dismissable = False, progress = -1,
# title = I18N_CATALOG.i18nc("@info:title", "Sending Data"),
# use_inactivity_timer = False)
# self._write_job_progress_message.show()
#
# if mesh_format.preferred_format is not None:
# self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream)
# job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)
# job.start()
# yield True # Return that we had success!
# yield # To prevent having to catch the StopIteration exception.
# def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
# if self._write_job_progress_message:
# self._write_job_progress_message.hide()
#
# self._progress_message = Message(I18N_CATALOG.i18nc("@info:status", "Sending data to printer"), lifetime = 0,
# dismissable = False, progress = -1,
# title = I18N_CATALOG.i18nc("@info:title", "Sending Data"))
# self._progress_message.addAction("Abort", I18N_CATALOG.i18nc("@action:button", "Cancel"), icon = "",
# description = "")
# self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
# self._progress_message.show()
# parts = []
#
# target_printer, preferred_format, stream = self._dummy_lambdas
#
# # If a specific printer was selected, it should be printed with that machine.
# if target_printer:
# target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
# parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))
#
# # Add user name to the print_job
# parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
#
# file_name = self._application.getPrintInformation().jobName + "." + preferred_format["extension"]
#
# output = stream.getvalue() # Either str or bytes depending on the output mode.
# if isinstance(stream, io.StringIO):
# output = cast(str, output).encode("utf-8")
# output = cast(bytes, output)
#
# parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))
#
# self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts,
# on_finished = self._onPostPrintJobFinished,
# on_progress = self._onUploadPrintJobProgress)
# def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
# if self._progress_message:
# self._progress_message.hide()
# self._compressing_gcode = False
# self._sending_gcode = False
# def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None:
# if bytes_total > 0:
# new_progress = bytes_sent / bytes_total * 100
# # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# # timeout responses if this happens.
# self._last_response_time = time()
# if self._progress_message is not None and new_progress != self._progress_message.getProgress():
# self._progress_message.show() # Ensure that the message is visible.
# self._progress_message.setProgress(bytes_sent / bytes_total * 100)
#
# # If successfully sent:
# if bytes_sent == bytes_total:
# # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to
# # the monitor tab.
# self._success_message = Message(
# I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
# lifetime=5, dismissable=True,
# title=I18N_CATALOG.i18nc("@info:title", "Data Sent"))
# self._success_message.addAction("View", I18N_CATALOG.i18nc("@action:button", "View in Monitor"), icon = "",
# description="")
# self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
# self._success_message.show()
# else:
# if self._progress_message is not None:
# self._progress_message.setProgress(0)
# self._progress_message.hide()
# def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
# if action_id == "Abort":
# Logger.log("d", "User aborted sending print to remote.")
# if self._progress_message is not None:
# self._progress_message.hide()
# self._compressing_gcode = False
# self._sending_gcode = False
# self._application.getController().setActiveStage("PrepareStage")
#
# # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
# # the "reply" should be disconnected
# if self._latest_reply_handler:
# self._latest_reply_handler.disconnect()
# self._latest_reply_handler = None
# def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
# if action_id == "View":
# self._application.getController().setActiveStage("MonitorStage")
# ## Callback for when preview image was downloaded from cluster.
# def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
# reply_url = reply.url().toString()
#
@ -317,53 +126,77 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# image.loadFromData(reply.readAll())
# print_job.updatePreviewImage(image)
# def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel":
# material_manager = self._application.getMaterialManager()
# material_group_list = None
#
# # Avoid crashing if there is no "guid" field in the metadata
# material_guid = material_data.get("guid")
# if material_guid:
# material_group_list = material_manager.getMaterialGroupListByGUID(material_guid)
#
# # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the
# # material is unknown to Cura, so we should return an "empty" or "unknown" material model.
# if material_group_list is None:
# material_name = I18N_CATALOG.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \
# else I18N_CATALOG.i18nc("@label:material", "Unknown")
#
# return MaterialOutputModel(guid = material_data.get("guid", ""),
# type = material_data.get("material", ""),
# color = material_data.get("color", ""),
# brand = material_data.get("brand", ""),
# name = material_data.get("name", material_name)
# )
#
# # Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
# read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
# non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list))
# material_group = None
# if read_only_material_group_list:
# read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
# material_group = read_only_material_group_list[0]
# elif non_read_only_material_group_list:
# non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
# material_group = non_read_only_material_group_list[0]
#
# if material_group:
# container = material_group.root_material_node.getContainer()
# color = container.getMetaDataEntry("color_code")
# brand = container.getMetaDataEntry("brand")
# material_type = container.getMetaDataEntry("material")
# name = container.getName()
# else:
# Logger.log("w",
# "Unable to find material with guid {guid}. Using data as provided by cluster".format(
# guid=material_data["guid"]))
# color = material_data["color"]
# brand = material_data["brand"]
# material_type = material_data["material"]
# name = I18N_CATALOG.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \
# else I18N_CATALOG.i18nc("@label:material", "Unknown")
# return MaterialOutputModel(guid = material_data["guid"], type = material_type,
# brand = brand, color = color, name = name)
## 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:
# Show an error message if we're already sending a job.
if self._progress.visible:
message = Message(
text=I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."),
title=I18N_CATALOG.i18nc("@info:title", "Print error"),
lifetime=10
)
message.show()
return
self.writeStarted.emit(self)
# Make sure the printer is aware of all new materials as the new print job might contain one.
self.sendMaterialProfiles()
# Detect the correct export format depending on printer type and firmware version.
mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
if not mesh_format.is_valid:
Logger.log("e", "Missing file or mesh writer!")
return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job."))
# Determine the filename.
job_name = CuraApplication.getInstance().getPrintInformation().jobName
extension = mesh_format.preferred_format.get("extension", "")
file_name = f"{job_name}.{extension}"
# Export the file.
stream = mesh_format.createStream()
job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode)
job.setFileName(file_name)
job.finished.connect(self._onPrintJobCreated)
job.start()
## Handler for when the print job was created locally.
# It can now be sent over the network.
def _onPrintJobCreated(self, job: WriteFileJob) -> None:
self._progress.show()
parts = []
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
output = job.getStream().getvalue()
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), output))
self.postFormWithParts("print_jobs/", parts, on_finished=self._onPrintUploadCompleted,
on_progress=self._onPrintJobUploadProgress)
## Handler for print job upload progress.
def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None:
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) -> None:
self._progress.hide()
Message(
text=I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
title=I18N_CATALOG.i18nc("@info:title", "Data Sent"),
lifetime=5
).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:
self._progress.hide()
Message(
text=message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),
title=I18N_CATALOG.i18nc("@info:title", "Network error"),
lifetime=10
).show()
self.writeError.emit()

View file

@ -8,11 +8,11 @@ I18N_CATALOG = i18nCatalog("cura")
## Class responsible for showing a progress message while a mesh is being uploaded to the cloud.
class CloudProgressMessage(Message):
class PrintJobUploadProgressMessage(Message):
def __init__(self):
super().__init__(
title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"),
text = I18N_CATALOG.i18nc("@info:status", "Uploading via Ultimaker Cloud"),
text = I18N_CATALOG.i18nc("@info:status", "Uploading print job to printer."),
progress = -1,
lifetime = 0,
dismissable = False,

View file

@ -14,6 +14,7 @@ from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from .Utils import formatTimeCompleted, formatDateCompleted
from .ClusterOutputController import ClusterOutputController
from .PrintJobUploadProgressMessage import PrintJobUploadProgressMessage
from .Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel
from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus
from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
@ -60,6 +61,9 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
# Load the Monitor UI elements.
self._loadMonitorTab()
# The job upload progress message modal.
self._progress = PrintJobUploadProgressMessage()
## The IP address of the printer.
@pyqtProperty(str, constant=True)
def address(self) -> str: