This commit is contained in:
ChrisTerBeke 2019-07-30 13:09:29 +02:00
parent 9e4c71cce3
commit bfca117bff
6 changed files with 119 additions and 110 deletions

View file

@ -9,6 +9,7 @@ from PyQt5.QtGui import QDesktopServices
from UM import i18nCatalog
from UM.Backend.Backend import BackendState
from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.WriteFileJob import WriteFileJob
from UM.Logger import Logger
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
@ -16,6 +17,7 @@ from UM.Version import Version
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob
from .CloudApiClient import CloudApiClient
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
@ -93,6 +95,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]]
# Reference to the uploaded print job / mesh
# We do this to prevent re-uploading the same file multiple times.
self._tool_path = None # type: Optional[bytes]
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
@ -137,43 +140,6 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
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:
# 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
if self._uploaded_print_job:
# The mesh didn't change, let's not upload it again
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
return
# Indicate we have started sending a job.
self.writeStarted.emit(self)
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."))
# 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,
file_size=len(mesh),
content_type=mesh_format.mime_type,
)
self._api.requestUpload(request, self._onPrintJobCreated)
## Called when the network data should be updated.
def _update(self) -> None:
super()._update()
@ -200,13 +166,51 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._received_print_jobs = status.print_jobs
self._updatePrintJobs(status.print_jobs)
## 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:
# 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
if self._uploaded_print_job:
# The mesh didn't change, let's not upload it again
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
return
# Indicate we have started sending a job.
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()
## Handler for when the print job was created locally.
# It can now be sent over the cloud.
def _onPrintJobCreated(self, job: WriteFileJob) -> None:
self._progress.show()
self._tool_path = job.getOutput()
request = CloudPrintJobUploadRequest(
job_name=job.getFileName(),
file_size=len(self._tool_path),
content_type=job.getMimeType(),
)
self._api.requestUpload(request, self._uploadPrintJob)
## Uploads the mesh when the print job was registered with the cloud API.
# \param job_response: The response received from the cloud API.
def _onPrintJobCreated(self, job_response: CloudPrintJobResponse) -> None:
def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None:
self._progress.show()
self._uploaded_print_job = job_response
tool_path = cast(bytes, self._tool_path)
self._api.uploadToolPath(job_response, tool_path, self._onPrintJobUploaded, self._progress.update,
self._uploaded_print_job = job_response # store the last uploaded job to prevent re-upload of the same file
self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
self._onUploadError)
## Requests the print to be sent to the printer when we finished uploading the mesh.

View file

@ -40,6 +40,7 @@ class CloudOutputDeviceManager:
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._api = CloudApiClient(self._account, self._onApiError)
self._account.loginStateChanged.connect(self._onLoginStateChanged)
# Create a timer to update the remote cluster list
self._update_timer = QTimer()
@ -53,17 +54,22 @@ class CloudOutputDeviceManager:
def start(self):
if self._running:
return
self._account.loginStateChanged.connect(self._onLoginStateChanged)
if not self._account.isLoggedIn:
return
self._running = True
if not self._update_timer.isActive():
self._update_timer.start()
self._update_timer.timeout.connect(self._getRemoteClusters)
self._onLoginStateChanged(is_logged_in=self._account.isLoggedIn)
## Stops running the cloud output device manager.
def stop(self):
if not self._running:
return
self._account.loginStateChanged.disconnect(self._onLoginStateChanged)
self._running = False
if self._update_timer.isActive():
self._update_timer.stop()
self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices.
self._update_timer.timeout.disconnect(self._getRemoteClusters)
self._onLoginStateChanged(is_logged_in=False)
## Force refreshing connections.
def refreshConnections(self) -> None:
@ -72,17 +78,13 @@ class CloudOutputDeviceManager:
## Called when the uses logs in or out
def _onLoginStateChanged(self, is_logged_in: bool) -> None:
if is_logged_in:
if not self._update_timer.isActive():
self._update_timer.start()
self._getRemoteClusters()
self.start()
else:
if self._update_timer.isActive():
self._update_timer.stop()
# Notify that all clusters have disappeared
self._onGetRemoteClustersFinished([])
self.stop()
## Gets all remote clusters from the API.
def _getRemoteClusters(self) -> None:
print("getRemoteClusters")
self._api.getClusters(self._onGetRemoteClustersFinished)
## Callback for when the request for getting the clusters is finished.
@ -113,6 +115,8 @@ class CloudOutputDeviceManager:
keys = self._remote_clusters.keys()
removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys]
for device in removed_devices:
device.disconnect()
device.close()
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key)
discovery.removeDiscoveredPrinter(device.key)
@ -127,10 +131,12 @@ class CloudOutputDeviceManager:
return
# The newly added machine is automatically activated.
CuraApplication.getInstance().getMachineManager().addMachine(device.printerType,
device.clusterData.friendly_name)
machine_manager = CuraApplication.getInstance().getMachineManager()
machine_manager.addMachine(device.printerType, device.clusterData.friendly_name)
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if active_machine:
if not active_machine:
return
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
self._connectToOutputDevice(device, active_machine)
## Callback for when the active machine was changed by the user or a new remote cluster was found.
@ -150,28 +156,25 @@ class CloudOutputDeviceManager:
device = self._remote_clusters[stored_cluster_id]
self._connectToOutputDevice(device, active_machine)
Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id)
else:
self._connectByNetworkKey(active_machine)
# else:
# self._connectByNetworkKey(active_machine)
## Tries to match the local network key to the cloud cluster host name.
def _connectByNetworkKey(self, active_machine: GlobalStack) -> None:
# Check if the active printer has a local network connection and match this key to the remote cluster.
local_network_key = active_machine.getMetaDataEntry("um_network_key")
if not local_network_key:
return
device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None)
if not device:
return
Logger.log("i", "Found cluster %s with network key %s", device, local_network_key)
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
self._connectToOutputDevice(device, active_machine)
## Connects to an output device and makes sure it is registered in the output device manager.
def _connectToOutputDevice(self, device: CloudOutputDevice, active_machine: GlobalStack) -> None:
@staticmethod
def _connectToOutputDevice(device: CloudOutputDevice, active_machine: GlobalStack) -> None:
device.connect()
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
active_machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)

View file

@ -0,0 +1,39 @@
from typing import List
from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.WriteFileJob import WriteFileJob
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
from .MeshFormatHandler import MeshFormatHandler
## Job that exports the build plate to the correct file format for the target cluster.
class ExportFileJob(WriteFileJob):
def __init__(self, file_handler: FileHandler, nodes: List[SceneNode], firmware_version: str) -> None:
self._mesh_format_handler = MeshFormatHandler(file_handler, firmware_version)
if not self._mesh_format_handler.is_valid:
Logger.log("e", "Missing file or mesh writer!")
return
# Determine the filename.
job_name = CuraApplication.getInstance().getPrintInformation().jobName
extension = self._mesh_format_handler.preferred_format.get("extension", "")
self.setFileName(f"{job_name}.{extension}")
super().__init__(self._mesh_format_handler.writer, self._mesh_format_handler.createStream(), nodes,
self._mesh_format_handler.file_mode)
## Get the mime type of the selected export file type.
def getMimeType(self) -> str:
return self._mesh_format_handler.mime_type
## Get the job result as bytes as that is what we need to upload to the cluster.
def getOutput(self) -> bytes:
output = self.getStream().getvalue()
if isinstance(output, str):
output = output.encode("utf-8")
return output

View file

@ -15,6 +15,7 @@ from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob
from .ClusterApiClient import ClusterApiClient
from ..MeshFormatHandler import MeshFormatHandler
@ -46,7 +47,6 @@ class NetworkOutputDevice(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:
self.setPriority(3) # Make sure the output device gets selected above local file output
@ -146,21 +146,8 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# 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(writer=mesh_format.writer, stream=stream, data=nodes, mode=mesh_format.file_mode)
job.setFileName(file_name)
# 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()
@ -168,14 +155,10 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# It can now be sent over the network.
def _onPrintJobCreated(self, job: WriteFileJob) -> None:
self._progress.show()
# TODO: extract multi-part stuff
parts = []
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
output = job.getStream().getvalue()
if isinstance(output, str):
# Ensure that our output is bytes
output = output.encode("utf-8")
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), output))
parts = [
self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"),
self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), job.getOutput())
]
self.postFormWithParts("/cluster-api/v1/print_jobs/", parts, on_finished=self._onPrintUploadCompleted,
on_progress=self._onPrintJobUploadProgress)

View file

@ -118,8 +118,6 @@ class NetworkOutputDeviceManager:
@staticmethod
def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None:
device.connect()
active_machine.setMetaDataEntry("um_network_key", device.key)
active_machine.setMetaDataEntry("group_name", device.name)
active_machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)
@ -199,6 +197,8 @@ class NetworkOutputDeviceManager:
# The newly added machine is automatically activated.
CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name)
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
active_machine.setMetaDataEntry("um_network_key", device.key)
active_machine.setMetaDataEntry("group_name", device.name)
if active_machine:
self._connectToOutputDevice(device, active_machine)

View file

@ -14,8 +14,6 @@ from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing.
class UM3OutputDevicePlugin(OutputDevicePlugin):
# cloudFlowIsPossible = Signal()
def __init__(self) -> None:
super().__init__()
@ -29,24 +27,6 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
# This ensures no output devices are still connected that do not belong to the new active machine.
CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections)
# TODO: re-write cloud messaging
# self._account = self._application.getCuraAPI().account
# Check if cloud flow is possible when user logs in
# self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)
# Check if cloud flow is possible when user switches machines
# self._application.globalContainerStackChanged.connect(self._onMachineSwitched)
# Listen for when cloud flow is possible
# self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)
# self._start_cloud_flow_message = None # type: Optional[Message]
# self._cloud_flow_complete_message = None # type: Optional[Message]
# self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured)
# self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible)
## Start looking for devices in the network and cloud.
def start(self):
self._network_output_device_manager.start()