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 import i18nCatalog
from UM.Backend.Backend import BackendState from UM.Backend.Backend import BackendState
from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.WriteFileJob import WriteFileJob
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -16,6 +17,7 @@ from UM.Version import Version
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob
from .CloudApiClient import CloudApiClient from .CloudApiClient import CloudApiClient
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
@ -93,6 +95,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]] self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]]
# Reference to the uploaded print job / mesh # 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._tool_path = None # type: Optional[bytes]
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
@ -129,7 +132,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
return True return True
return False return False
## Set all the interface elements and texts for this output device. ## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None: def _setInterfaceElements(self) -> None:
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'. self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'.
self.setName(self._id) self.setName(self._id)
@ -137,6 +140,32 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
## Called when the network data should be updated.
def _update(self) -> None:
super()._update()
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
return # Avoid calling the cloud too often
Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
if self._account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated)
self._last_request_time = time()
self._api.getClusterStatus(self.key, self._onStatusCallFinished)
else:
self.setAuthenticationState(AuthState.NotAuthenticated)
## Method called when HTTP request to status endpoint is finished.
# Contains both printers and print jobs statuses in a single response.
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
# Update all data from the cluster.
self._last_response_time = time()
if self._received_printers != status.printers:
self._received_printers = status.printers
self._updatePrinters(status.printers)
if status.print_jobs != self._received_print_jobs:
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. ## 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, 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: file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
@ -159,54 +188,29 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# Indicate we have started sending a job. # Indicate we have started sending a job.
self.writeStarted.emit(self) self.writeStarted.emit(self)
mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) # Export the scene to the correct file type.
if not mesh_format.is_valid: job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion)
Logger.log("e", "Missing file or mesh writer!") job.finished.connect(self._onPrintJobCreated)
return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job.")) job.start()
# TODO: use stream just like the network output device ## Handler for when the print job was created locally.
mesh = mesh_format.getBytes(nodes) # It can now be sent over the cloud.
self._tool_path = mesh def _onPrintJobCreated(self, job: WriteFileJob) -> None:
self._progress.show()
self._tool_path = job.getOutput()
request = CloudPrintJobUploadRequest( request = CloudPrintJobUploadRequest(
job_name=file_name or mesh_format.file_extension, job_name=job.getFileName(),
file_size=len(mesh), file_size=len(self._tool_path),
content_type=mesh_format.mime_type, content_type=job.getMimeType(),
) )
self._api.requestUpload(request, self._onPrintJobCreated) self._api.requestUpload(request, self._uploadPrintJob)
## Called when the network data should be updated.
def _update(self) -> None:
super()._update()
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
return # Avoid calling the cloud too often
Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
if self._account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated)
self._last_request_time = time()
self._api.getClusterStatus(self.key, self._onStatusCallFinished)
else:
self.setAuthenticationState(AuthState.NotAuthenticated)
## Method called when HTTP request to status endpoint is finished.
# Contains both printers and print jobs statuses in a single response.
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
# Update all data from the cluster.
self._last_response_time = time()
if self._received_printers != status.printers:
self._received_printers = status.printers
self._updatePrinters(status.printers)
if status.print_jobs != self._received_print_jobs:
self._received_print_jobs = status.print_jobs
self._updatePrintJobs(status.print_jobs)
## Uploads the mesh when the print job was registered with the cloud API. ## Uploads the mesh when the print job was registered with the cloud API.
# \param job_response: The response received from 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._progress.show()
self._uploaded_print_job = job_response self._uploaded_print_job = job_response # store the last uploaded job to prevent re-upload of the same file
tool_path = cast(bytes, self._tool_path) self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
self._api.uploadToolPath(job_response, tool_path, self._onPrintJobUploaded, self._progress.update,
self._onUploadError) self._onUploadError)
## Requests the print to be sent to the printer when we finished uploading the mesh. ## 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._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._api = CloudApiClient(self._account, self._onApiError) self._api = CloudApiClient(self._account, self._onApiError)
self._account.loginStateChanged.connect(self._onLoginStateChanged)
# Create a timer to update the remote cluster list # Create a timer to update the remote cluster list
self._update_timer = QTimer() self._update_timer = QTimer()
@ -53,17 +54,22 @@ class CloudOutputDeviceManager:
def start(self): def start(self):
if self._running: if self._running:
return 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._update_timer.timeout.connect(self._getRemoteClusters)
self._onLoginStateChanged(is_logged_in=self._account.isLoggedIn)
## Stops running the cloud output device manager. ## Stops running the cloud output device manager.
def stop(self): def stop(self):
if not self._running: if not self._running:
return 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._update_timer.timeout.disconnect(self._getRemoteClusters)
self._onLoginStateChanged(is_logged_in=False)
## Force refreshing connections. ## Force refreshing connections.
def refreshConnections(self) -> None: def refreshConnections(self) -> None:
@ -72,17 +78,13 @@ class CloudOutputDeviceManager:
## Called when the uses logs in or out ## Called when the uses logs in or out
def _onLoginStateChanged(self, is_logged_in: bool) -> None: def _onLoginStateChanged(self, is_logged_in: bool) -> None:
if is_logged_in: if is_logged_in:
if not self._update_timer.isActive(): self.start()
self._update_timer.start()
self._getRemoteClusters()
else: else:
if self._update_timer.isActive(): self.stop()
self._update_timer.stop()
# Notify that all clusters have disappeared
self._onGetRemoteClustersFinished([])
## Gets all remote clusters from the API. ## Gets all remote clusters from the API.
def _getRemoteClusters(self) -> None: def _getRemoteClusters(self) -> None:
print("getRemoteClusters")
self._api.getClusters(self._onGetRemoteClustersFinished) self._api.getClusters(self._onGetRemoteClustersFinished)
## Callback for when the request for getting the clusters is finished. ## Callback for when the request for getting the clusters is finished.
@ -113,6 +115,8 @@ class CloudOutputDeviceManager:
keys = self._remote_clusters.keys() keys = self._remote_clusters.keys()
removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys] removed_devices = [cluster for cluster in self._remote_clusters.values() if cluster.key not in keys]
for device in removed_devices: for device in removed_devices:
device.disconnect()
device.close()
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key) CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(device.key)
discovery.removeDiscoveredPrinter(device.key) discovery.removeDiscoveredPrinter(device.key)
@ -127,11 +131,13 @@ class CloudOutputDeviceManager:
return return
# The newly added machine is automatically activated. # The newly added machine is automatically activated.
CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, machine_manager = CuraApplication.getInstance().getMachineManager()
device.clusterData.friendly_name) machine_manager.addMachine(device.printerType, device.clusterData.friendly_name)
active_machine = CuraApplication.getInstance().getGlobalContainerStack() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
if active_machine: if not active_machine:
self._connectToOutputDevice(device, 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. ## Callback for when the active machine was changed by the user or a new remote cluster was found.
def _connectToActiveMachine(self) -> None: def _connectToActiveMachine(self) -> None:
@ -150,28 +156,25 @@ class CloudOutputDeviceManager:
device = self._remote_clusters[stored_cluster_id] device = self._remote_clusters[stored_cluster_id]
self._connectToOutputDevice(device, active_machine) self._connectToOutputDevice(device, active_machine)
Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id) Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id)
else: # else:
self._connectByNetworkKey(active_machine) # self._connectByNetworkKey(active_machine)
## Tries to match the local network key to the cloud cluster host name. ## Tries to match the local network key to the cloud cluster host name.
def _connectByNetworkKey(self, active_machine: GlobalStack) -> None: 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") local_network_key = active_machine.getMetaDataEntry("um_network_key")
if not local_network_key: if not local_network_key:
return return
device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None) device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None)
if not device: if not device:
return return
Logger.log("i", "Found cluster %s with network key %s", device, local_network_key) Logger.log("i", "Found cluster %s with network key %s", device, local_network_key)
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
self._connectToOutputDevice(device, active_machine) self._connectToOutputDevice(device, active_machine)
## Connects to an output device and makes sure it is registered in the output device manager. ## 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() device.connect()
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
active_machine.addConfiguredConnectionType(device.connectionType.value) active_machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) 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.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from plugins.UM3NetworkPrinting.src.ExportFileJob import ExportFileJob
from .ClusterApiClient import ClusterApiClient from .ClusterApiClient import ClusterApiClient
from ..MeshFormatHandler import MeshFormatHandler from ..MeshFormatHandler import MeshFormatHandler
@ -46,7 +47,6 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self._setInterfaceElements() self._setInterfaceElements()
self._active_camera_url = QUrl() # type: QUrl self._active_camera_url = QUrl() # type: QUrl
## Set all the interface elements and texts for this output device. ## Set all the interface elements and texts for this output device.
def _setInterfaceElements(self) -> None: def _setInterfaceElements(self) -> None:
self.setPriority(3) # Make sure the output device gets selected above local file output 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. # Make sure the printer is aware of all new materials as the new print job might contain one.
self.sendMaterialProfiles() self.sendMaterialProfiles()
# Detect the correct export format depending on printer type and firmware version. # Export the scene to the correct file type.
mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion) job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=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)
job.finished.connect(self._onPrintJobCreated) job.finished.connect(self._onPrintJobCreated)
job.start() job.start()
@ -168,14 +155,10 @@ class NetworkOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# It can now be sent over the network. # It can now be sent over the network.
def _onPrintJobCreated(self, job: WriteFileJob) -> None: def _onPrintJobCreated(self, job: WriteFileJob) -> None:
self._progress.show() self._progress.show()
# TODO: extract multi-part stuff parts = [
parts = [] self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"),
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) self._createFormPart("name=\"file\"; filename=\"%s\"" % job.getFileName(), job.getOutput())
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))
self.postFormWithParts("/cluster-api/v1/print_jobs/", parts, on_finished=self._onPrintUploadCompleted, self.postFormWithParts("/cluster-api/v1/print_jobs/", parts, on_finished=self._onPrintUploadCompleted,
on_progress=self._onPrintJobUploadProgress) on_progress=self._onPrintJobUploadProgress)

View file

@ -118,8 +118,6 @@ class NetworkOutputDeviceManager:
@staticmethod @staticmethod
def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None: def _connectToOutputDevice(device: PrinterOutputDevice, active_machine: GlobalStack) -> None:
device.connect() device.connect()
active_machine.setMetaDataEntry("um_network_key", device.key)
active_machine.setMetaDataEntry("group_name", device.name)
active_machine.addConfiguredConnectionType(device.connectionType.value) active_machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)
@ -199,6 +197,8 @@ class NetworkOutputDeviceManager:
# The newly added machine is automatically activated. # The newly added machine is automatically activated.
CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name) CuraApplication.getInstance().getMachineManager().addMachine(device.printerType, device.name)
active_machine = CuraApplication.getInstance().getGlobalContainerStack() active_machine = CuraApplication.getInstance().getGlobalContainerStack()
active_machine.setMetaDataEntry("um_network_key", device.key)
active_machine.setMetaDataEntry("group_name", device.name)
if active_machine: if active_machine:
self._connectToOutputDevice(device, 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. ## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing.
class UM3OutputDevicePlugin(OutputDevicePlugin): class UM3OutputDevicePlugin(OutputDevicePlugin):
# cloudFlowIsPossible = Signal()
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() 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. # This ensures no output devices are still connected that do not belong to the new active machine.
CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) 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. ## Start looking for devices in the network and cloud.
def start(self): def start(self):
self._network_output_device_manager.start() self._network_output_device_manager.start()