mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 06:57:28 -06:00
STAR-322: Improving configuration models interface
This commit is contained in:
parent
a1c252d3a2
commit
4f82a2759a
7 changed files with 278 additions and 183 deletions
|
@ -4,6 +4,7 @@
|
||||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||||
|
@ -162,9 +163,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
part.setBody(data)
|
part.setBody(data)
|
||||||
return part
|
return part
|
||||||
|
|
||||||
## Convenience function to get the username from the OS.
|
## Convenience function to get the username, either from the cloud or from the OS.
|
||||||
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
|
|
||||||
def _getUserName(self) -> str:
|
def _getUserName(self) -> str:
|
||||||
|
# check first if we are logged in with the Ultimaker Account
|
||||||
|
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||||
|
if account and account.isLoggedIn:
|
||||||
|
return account.userName
|
||||||
|
|
||||||
|
# Otherwise get the username from the US
|
||||||
|
# The code below was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||||
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
||||||
user = os.environ.get(name)
|
user = os.environ.get(name)
|
||||||
if user:
|
if user:
|
||||||
|
|
|
@ -16,7 +16,6 @@ from UM.Qt.Duration import Duration, DurationFormat
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController
|
from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController
|
||||||
from ..MeshFormatHandler import MeshFormatHandler
|
from ..MeshFormatHandler import MeshFormatHandler
|
||||||
|
@ -28,7 +27,7 @@ from .Models.CloudPrintResponse import CloudPrintResponse
|
||||||
from .Models.CloudJobResponse import CloudJobResponse
|
from .Models.CloudJobResponse import CloudJobResponse
|
||||||
from .Models.CloudClusterPrinter import CloudClusterPrinter
|
from .Models.CloudClusterPrinter import CloudClusterPrinter
|
||||||
from .Models.CloudClusterPrintJob import CloudClusterPrintJob
|
from .Models.CloudClusterPrintJob import CloudClusterPrintJob
|
||||||
from .Utils import findChanges
|
from .Utils import findChanges, formatDateCompleted, formatTimeCompleted
|
||||||
|
|
||||||
|
|
||||||
## Class that contains all the translations for this module.
|
## Class that contains all the translations for this module.
|
||||||
|
@ -55,6 +54,12 @@ class T:
|
||||||
UPLOAD_SUCCESS_TITLE = _I18N_CATALOG.i18nc("@info:title", "Data Sent")
|
UPLOAD_SUCCESS_TITLE = _I18N_CATALOG.i18nc("@info:title", "Data Sent")
|
||||||
UPLOAD_SUCCESS_TEXT = _I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer.")
|
UPLOAD_SUCCESS_TEXT = _I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer.")
|
||||||
|
|
||||||
|
JOB_COMPLETED_TITLE = _I18N_CATALOG.i18nc("@info:status", "Print finished")
|
||||||
|
JOB_COMPLETED_PRINTER = _I18N_CATALOG.i18nc("@info:status",
|
||||||
|
"Printer '{printer_name}' has finished printing '{job_name}'.")
|
||||||
|
|
||||||
|
JOB_COMPLETED_NO_PRINTER = _I18N_CATALOG.i18nc("@info:status", "The print job '{job_name}' was finished.")
|
||||||
|
|
||||||
|
|
||||||
## The cloud output device is a network output device that works remotely but has limited functionality.
|
## The cloud output device is a network output device that works remotely but has limited functionality.
|
||||||
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||||
|
@ -65,7 +70,7 @@ class T:
|
||||||
class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
# The interval with which the remote clusters are checked
|
# The interval with which the remote clusters are checked
|
||||||
CHECK_CLUSTER_INTERVAL = 2.0 # seconds
|
CHECK_CLUSTER_INTERVAL = 4.0 # seconds
|
||||||
|
|
||||||
# Signal triggered when the print jobs in the queue were changed.
|
# Signal triggered when the print jobs in the queue were changed.
|
||||||
printJobsChanged = pyqtSignal()
|
printJobsChanged = pyqtSignal()
|
||||||
|
@ -109,6 +114,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
# We only allow a single upload at a time.
|
# We only allow a single upload at a time.
|
||||||
self._sending_job = False
|
self._sending_job = False
|
||||||
|
# TODO: handle progress messages in another class.
|
||||||
self._progress_message = None # type: Optional[Message]
|
self._progress_message = None # type: Optional[Message]
|
||||||
|
|
||||||
## Gets the host name of this device
|
## Gets the host name of this device
|
||||||
|
@ -128,7 +134,7 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
return network_key.startswith(self._host_name)
|
return network_key.startswith(self._host_name)
|
||||||
|
|
||||||
## 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):
|
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)
|
||||||
self.setShortDescription(T.PRINT_VIA_CLOUD_BUTTON)
|
self.setShortDescription(T.PRINT_VIA_CLOUD_BUTTON)
|
||||||
|
@ -157,13 +163,192 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
# TODO: Remove extension from the file name, since we are using content types now
|
# TODO: Remove extension from the file name, since we are using content types now
|
||||||
request = CloudJobUploadRequest(
|
request = CloudJobUploadRequest(
|
||||||
job_name = file_name + "." + mesh_format.file_extension,
|
job_name = file_name, ## + "." + mesh_format.file_extension,
|
||||||
file_size = len(mesh_bytes),
|
file_size = len(mesh_bytes),
|
||||||
content_type = mesh_format.mime_type,
|
content_type = mesh_format.mime_type,
|
||||||
)
|
)
|
||||||
self._api.requestUpload(request, lambda response: self._onPrintJobCreated(mesh_bytes, response))
|
self._api.requestUpload(request, lambda response: self._onPrintJobCreated(mesh_bytes, response))
|
||||||
|
|
||||||
## Get remote printers.
|
## Called when the connection to the cluster changes.
|
||||||
|
def connect(self) -> None:
|
||||||
|
super().connect()
|
||||||
|
|
||||||
|
## Called when the network data should be updated.
|
||||||
|
def _update(self) -> None:
|
||||||
|
super()._update()
|
||||||
|
if self._last_response_time and time() - self._last_response_time < self.CHECK_CLUSTER_INTERVAL:
|
||||||
|
return # avoid calling the cloud too often
|
||||||
|
|
||||||
|
if self._account.isLoggedIn:
|
||||||
|
self.setAuthenticationState(AuthState.Authenticated)
|
||||||
|
self._api.getClusterStatus(self._device_id, 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._updatePrinters(status.printers)
|
||||||
|
self._updatePrintJobs(status.print_jobs)
|
||||||
|
|
||||||
|
## Updates the local list of printers with the list received from the cloud.
|
||||||
|
# \param jobs: The printers received from the cloud.
|
||||||
|
def _updatePrinters(self, printers: List[CloudClusterPrinter]) -> None:
|
||||||
|
previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel]
|
||||||
|
received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinter]
|
||||||
|
|
||||||
|
removed_printers, added_printers, updated_printers = findChanges(previous, received)
|
||||||
|
|
||||||
|
for removed_printer in removed_printers:
|
||||||
|
if self._active_printer == removed_printer:
|
||||||
|
self.setActivePrinter(None)
|
||||||
|
self._printers.remove(removed_printer)
|
||||||
|
|
||||||
|
for added_printer in added_printers:
|
||||||
|
self._printers.append(added_printer.createOutputModel(CloudOutputController(self)))
|
||||||
|
|
||||||
|
for model, printer in updated_printers:
|
||||||
|
printer.updateOutputModel(model)
|
||||||
|
|
||||||
|
# Always have an active printer
|
||||||
|
if not self._active_printer:
|
||||||
|
self.setActivePrinter(self._printers[0])
|
||||||
|
|
||||||
|
if removed_printers or added_printers or updated_printers:
|
||||||
|
self._clusterPrintersChanged.emit()
|
||||||
|
|
||||||
|
## Updates the local list of print jobs with the list received from the cloud.
|
||||||
|
# \param jobs: The print jobs received from the cloud.
|
||||||
|
def _updatePrintJobs(self, jobs: List[CloudClusterPrintJob]) -> None:
|
||||||
|
received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJob]
|
||||||
|
previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel]
|
||||||
|
|
||||||
|
removed_jobs, added_jobs, updated_jobs = findChanges(previous, received)
|
||||||
|
|
||||||
|
# TODO: we see that not all data in the UI is correctly updated when the queue and active jobs change.
|
||||||
|
# TODO: we need to fix this here somehow by updating the correct output models.
|
||||||
|
# TODO: the configuration drop down in the slice window is not populated because we are missing some data.
|
||||||
|
# TODO: to fix this we need to implement more data as shown in ClusterUM3OutputDevice._createPrintJobModel
|
||||||
|
|
||||||
|
for removed_job in removed_jobs:
|
||||||
|
self._print_jobs.remove(removed_job)
|
||||||
|
|
||||||
|
for added_job in added_jobs:
|
||||||
|
self._addPrintJob(added_job)
|
||||||
|
|
||||||
|
for model, job in updated_jobs:
|
||||||
|
job.updateOutputModel(model)
|
||||||
|
if job.printer_uuid:
|
||||||
|
self._updateAssignedPrinter(model, job.printer_uuid)
|
||||||
|
|
||||||
|
# We only have to update when jobs are added or removed
|
||||||
|
# updated jobs push their changes via their output model
|
||||||
|
if added_jobs or removed_jobs or updated_jobs:
|
||||||
|
self.printJobsChanged.emit()
|
||||||
|
|
||||||
|
## Registers a new print job received via the cloud API.
|
||||||
|
# \param job: The print job received.
|
||||||
|
def _addPrintJob(self, job: CloudClusterPrintJob) -> None:
|
||||||
|
model = job.createOutputModel(CloudOutputController(self))
|
||||||
|
model.stateChanged.connect(self._onPrintJobStateChanged)
|
||||||
|
if job.printer_uuid:
|
||||||
|
self._updateAssignedPrinter(model, job.printer_uuid)
|
||||||
|
self._print_jobs.append(model)
|
||||||
|
|
||||||
|
## Handles the event of a change in a print job state
|
||||||
|
def _onPrintJobStateChanged(self) -> None:
|
||||||
|
username = self._account.userName
|
||||||
|
finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]
|
||||||
|
|
||||||
|
newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
|
||||||
|
for job in newly_finished_jobs:
|
||||||
|
if job.assignedPrinter:
|
||||||
|
job_completed_text = T.JOB_COMPLETED_PRINTER.format(printer_name=job.assignedPrinter.name,
|
||||||
|
job_name=job.name)
|
||||||
|
else:
|
||||||
|
job_completed_text = T.JOB_COMPLETED_NO_PRINTER.format(job_name=job.name)
|
||||||
|
job_completed_message = Message(text=job_completed_text, title = T.JOB_COMPLETED_TITLE)
|
||||||
|
job_completed_message.show()
|
||||||
|
|
||||||
|
# Ensure UI gets updated
|
||||||
|
self.printJobsChanged.emit()
|
||||||
|
|
||||||
|
## Updates the printer assignment for the given print job model.
|
||||||
|
def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None:
|
||||||
|
printer = next((p for p in self._printers if printer_uuid == p.key), None)
|
||||||
|
|
||||||
|
if not printer:
|
||||||
|
return Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key,
|
||||||
|
[p.key for p in self._printers])
|
||||||
|
|
||||||
|
printer.updateActivePrintJob(model)
|
||||||
|
model.updateAssignedPrinter(printer)
|
||||||
|
|
||||||
|
## Uploads the mesh when the print job was registered with the cloud API.
|
||||||
|
# \param mesh: The bytes to upload.
|
||||||
|
# \param job_response: The response received from the cloud API.
|
||||||
|
def _onPrintJobCreated(self, mesh: bytes, job_response: CloudJobResponse) -> None:
|
||||||
|
self._api.uploadMesh(job_response, mesh, self._onPrintJobUploaded, self._updateUploadProgress,
|
||||||
|
lambda _: self._onUploadError(T.UPLOAD_ERROR))
|
||||||
|
|
||||||
|
## Requests the print to be sent to the printer when we finished uploading the mesh.
|
||||||
|
# \param job_id: The ID of the job.
|
||||||
|
def _onPrintJobUploaded(self, job_id: str) -> None:
|
||||||
|
self._api.requestPrint(self._device_id, job_id, self._onUploadSuccess)
|
||||||
|
|
||||||
|
## Updates the progress of the mesh upload.
|
||||||
|
# \param progress: The amount of percentage points uploaded until now (0-100).
|
||||||
|
def _updateUploadProgress(self, progress: int) -> None:
|
||||||
|
if not self._progress_message:
|
||||||
|
self._progress_message = Message(
|
||||||
|
text = T.SENDING_DATA_TEXT,
|
||||||
|
title = T.SENDING_DATA_TITLE,
|
||||||
|
progress = -1,
|
||||||
|
lifetime = 0,
|
||||||
|
dismissable = False,
|
||||||
|
use_inactivity_timer = False
|
||||||
|
)
|
||||||
|
self._progress_message.setProgress(progress)
|
||||||
|
self._progress_message.show()
|
||||||
|
|
||||||
|
## Hides the upload progress bar
|
||||||
|
def _resetUploadProgress(self) -> None:
|
||||||
|
if self._progress_message:
|
||||||
|
self._progress_message.hide()
|
||||||
|
self._progress_message = None
|
||||||
|
|
||||||
|
## Displays the given message if uploading the mesh has failed
|
||||||
|
# \param message: The message to display.
|
||||||
|
def _onUploadError(self, message: str = None) -> None:
|
||||||
|
self._resetUploadProgress()
|
||||||
|
if message:
|
||||||
|
message = Message(
|
||||||
|
text = message,
|
||||||
|
title = T.ERROR,
|
||||||
|
lifetime = 10,
|
||||||
|
dismissable = True
|
||||||
|
)
|
||||||
|
message.show()
|
||||||
|
self._sending_job = False # the upload has finished so we're not sending a job anymore
|
||||||
|
self.writeError.emit()
|
||||||
|
|
||||||
|
## Shows a message when the upload has succeeded
|
||||||
|
# \param response: The response from the cloud API.
|
||||||
|
def _onUploadSuccess(self, response: CloudPrintResponse) -> None:
|
||||||
|
Logger.log("i", "The cluster will be printing this print job with the ID %s", response.cluster_job_id)
|
||||||
|
self._resetUploadProgress()
|
||||||
|
message = Message(
|
||||||
|
text = T.UPLOAD_SUCCESS_TEXT,
|
||||||
|
title = T.UPLOAD_SUCCESS_TITLE,
|
||||||
|
lifetime = 5,
|
||||||
|
dismissable = True,
|
||||||
|
)
|
||||||
|
message.show()
|
||||||
|
self._sending_job = False # the upload has finished so we're not sending a job anymore
|
||||||
|
self.writeFinished.emit()
|
||||||
|
|
||||||
|
## Gets the remote printers.
|
||||||
@pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
|
@pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
|
||||||
def printers(self) -> List[PrinterOutputModel]:
|
def printers(self) -> List[PrinterOutputModel]:
|
||||||
return self._printers
|
return self._printers
|
||||||
|
@ -209,170 +394,11 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
def getTimeCompleted(self, time_remaining: int) -> str:
|
def getTimeCompleted(self, time_remaining: int) -> str:
|
||||||
# TODO: this really shouldn't be in this class
|
# TODO: this really shouldn't be in this class
|
||||||
current_time = time()
|
return formatTimeCompleted(time_remaining)
|
||||||
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
|
||||||
return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute)
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
def getDateCompleted(self, time_remaining: int) -> str:
|
def getDateCompleted(self, time_remaining: int) -> str:
|
||||||
# TODO: this really shouldn't be in this class
|
return formatDateCompleted(time_remaining)
|
||||||
current_time = time()
|
|
||||||
completed = datetime.fromtimestamp(current_time + time_remaining)
|
|
||||||
today = datetime.fromtimestamp(current_time)
|
|
||||||
# If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format
|
|
||||||
if completed.toordinal() > today.toordinal() + 7:
|
|
||||||
return completed.strftime("%a %b ") + "{day}".format(day = completed.day)
|
|
||||||
# If finishing date is within the next week, use "Monday at HH:MM" format
|
|
||||||
elif completed.toordinal() > today.toordinal() + 1:
|
|
||||||
return completed.strftime("%a")
|
|
||||||
# If finishing tomorrow, use "tomorrow at HH:MM" format
|
|
||||||
elif completed.toordinal() > today.toordinal():
|
|
||||||
return "tomorrow"
|
|
||||||
# If finishing today, use "today at HH:MM" format
|
|
||||||
else:
|
|
||||||
return "today"
|
|
||||||
|
|
||||||
## Called when the connection to the cluster changes.
|
|
||||||
def connect(self) -> None:
|
|
||||||
super().connect()
|
|
||||||
|
|
||||||
## Called when the network data should be updated.
|
|
||||||
def _update(self) -> None:
|
|
||||||
super()._update()
|
|
||||||
if self._last_response_time and time() - self._last_response_time < self.CHECK_CLUSTER_INTERVAL:
|
|
||||||
return # avoid calling the cloud too often
|
|
||||||
|
|
||||||
if self._account.isLoggedIn:
|
|
||||||
self.setAuthenticationState(AuthState.Authenticated)
|
|
||||||
self._api.getClusterStatus(self._device_id, 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._updatePrinters(status.printers)
|
|
||||||
self._updatePrintJobs(status.print_jobs)
|
|
||||||
|
|
||||||
def _updatePrinters(self, printers: List[CloudClusterPrinter]) -> None:
|
|
||||||
previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel]
|
|
||||||
received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinter]
|
|
||||||
|
|
||||||
removed_printers, added_printers, updated_printers = findChanges(previous, received)
|
|
||||||
|
|
||||||
for removed_printer in removed_printers:
|
|
||||||
if self._active_printer == removed_printer:
|
|
||||||
self.setActivePrinter(None)
|
|
||||||
self._printers.remove(removed_printer)
|
|
||||||
|
|
||||||
for added_printer in added_printers:
|
|
||||||
self._printers.append(added_printer.createOutputModel(CloudOutputController(self)))
|
|
||||||
|
|
||||||
for model, printer in updated_printers:
|
|
||||||
printer.updateOutputModel(model)
|
|
||||||
|
|
||||||
# Always have an active printer
|
|
||||||
if not self._active_printer:
|
|
||||||
self.setActivePrinter(self._printers[0])
|
|
||||||
|
|
||||||
if removed_printers or added_printers or updated_printers:
|
|
||||||
self._clusterPrintersChanged.emit()
|
|
||||||
|
|
||||||
def _updatePrintJobs(self, jobs: List[CloudClusterPrintJob]) -> None:
|
|
||||||
received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJob]
|
|
||||||
previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel]
|
|
||||||
|
|
||||||
removed_jobs, added_jobs, updated_jobs = findChanges(previous, received)
|
|
||||||
|
|
||||||
# TODO: we see that not all data in the UI is correctly updated when the queue and active jobs change.
|
|
||||||
# TODO: we need to fix this here somehow by updating the correct output models.
|
|
||||||
# TODO: also the configuration drop down in the slice window is not populated because we are missing some data.
|
|
||||||
# TODO: to fix this we need to implement more data as shown in ClusterUM3OutputDevice._createPrintJobModel
|
|
||||||
|
|
||||||
for removed_job in removed_jobs:
|
|
||||||
self._print_jobs.remove(removed_job)
|
|
||||||
|
|
||||||
for added_job in added_jobs:
|
|
||||||
self._addPrintJob(added_job)
|
|
||||||
|
|
||||||
for model, job in updated_jobs:
|
|
||||||
job.updateOutputModel(model)
|
|
||||||
self._updatePrintJobDetails(model)
|
|
||||||
|
|
||||||
# We only have to update when jobs are added or removed
|
|
||||||
# updated jobs push their changes via their output model
|
|
||||||
if added_jobs or removed_jobs or updated_jobs:
|
|
||||||
self.printJobsChanged.emit()
|
|
||||||
|
|
||||||
def _addPrintJob(self, job: CloudClusterPrintJob) -> None:
|
|
||||||
print_job = job.createOutputModel(CloudOutputController(self))
|
|
||||||
self._updatePrintJobDetails(print_job)
|
|
||||||
self._print_jobs.append(print_job)
|
|
||||||
|
|
||||||
def _updatePrintJobDetails(self, print_job: UM3PrintJobOutputModel):
|
|
||||||
printer = None
|
|
||||||
try:
|
|
||||||
printer = next(p for p in self._printers if print_job.assignedPrinter == p.key)
|
|
||||||
except StopIteration:
|
|
||||||
Logger.log("w", "Missing printer %s for job %s in %s", print_job.assignedPrinter, print_job.key,
|
|
||||||
[p.key for p in self._printers])
|
|
||||||
|
|
||||||
if printer:
|
|
||||||
printer.updateActivePrintJob(print_job)
|
|
||||||
print_job.updateAssignedPrinter(printer)
|
|
||||||
|
|
||||||
def _onPrintJobCreated(self, mesh: bytes, job_response: CloudJobResponse) -> None:
|
|
||||||
self._api.uploadMesh(job_response, mesh, self._onPrintJobUploaded, self._updateUploadProgress,
|
|
||||||
lambda _: self._onUploadError(T.UPLOAD_ERROR))
|
|
||||||
|
|
||||||
def _onPrintJobUploaded(self, job_id: str) -> None:
|
|
||||||
self._api.requestPrint(self._device_id, job_id, self._onUploadSuccess)
|
|
||||||
|
|
||||||
def _updateUploadProgress(self, progress: int):
|
|
||||||
if not self._progress_message:
|
|
||||||
self._progress_message = Message(
|
|
||||||
text = T.SENDING_DATA_TEXT,
|
|
||||||
title = T.SENDING_DATA_TITLE,
|
|
||||||
progress = -1,
|
|
||||||
lifetime = 0,
|
|
||||||
dismissable = False,
|
|
||||||
use_inactivity_timer = False
|
|
||||||
)
|
|
||||||
self._progress_message.setProgress(progress)
|
|
||||||
self._progress_message.show()
|
|
||||||
|
|
||||||
def _resetUploadProgress(self):
|
|
||||||
if self._progress_message:
|
|
||||||
self._progress_message.hide()
|
|
||||||
self._progress_message = None
|
|
||||||
|
|
||||||
def _onUploadError(self, message: str = None):
|
|
||||||
self._resetUploadProgress()
|
|
||||||
if message:
|
|
||||||
message = Message(
|
|
||||||
text = message,
|
|
||||||
title = T.ERROR,
|
|
||||||
lifetime = 10,
|
|
||||||
dismissable = True
|
|
||||||
)
|
|
||||||
message.show()
|
|
||||||
self._sending_job = False # the upload has finished so we're not sending a job anymore
|
|
||||||
self.writeError.emit()
|
|
||||||
|
|
||||||
# Shows a message when the upload has succeeded
|
|
||||||
def _onUploadSuccess(self, response: CloudPrintResponse):
|
|
||||||
Logger.log("i", "The cluster will be printing this print job with the ID %s", response.cluster_job_id)
|
|
||||||
self._resetUploadProgress()
|
|
||||||
message = Message(
|
|
||||||
text = T.UPLOAD_SUCCESS_TEXT,
|
|
||||||
title = T.UPLOAD_SUCCESS_TITLE,
|
|
||||||
lifetime = 5,
|
|
||||||
dismissable = True,
|
|
||||||
)
|
|
||||||
message.show()
|
|
||||||
self._sending_job = False # the upload has finished so we're not sending a job anymore
|
|
||||||
self.writeFinished.emit()
|
|
||||||
|
|
||||||
## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud.
|
## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud.
|
||||||
# TODO: We fake the methods here to not break the monitor page.
|
# TODO: We fake the methods here to not break the monitor page.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController
|
from plugins.UM3NetworkPrinting.src.Cloud.CloudOutputController import CloudOutputController
|
||||||
from .CloudClusterPrinterConfiguration import CloudClusterPrinterConfiguration
|
from .CloudClusterPrinterConfiguration import CloudClusterPrinterConfiguration
|
||||||
from .CloudClusterPrintJobConstraint import CloudClusterPrintJobConstraint
|
from .CloudClusterPrintJobConstraint import CloudClusterPrintJobConstraint
|
||||||
|
@ -31,20 +32,33 @@ class CloudClusterPrintJob(BaseModel):
|
||||||
self.time_total = None # type: str
|
self.time_total = None # type: str
|
||||||
self.uuid = None # type: str
|
self.uuid = None # type: str
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.printers = [CloudClusterPrinterConfiguration(**c) if isinstance(c, dict) else c
|
self.configuration = [CloudClusterPrinterConfiguration(**c) if isinstance(c, dict) else c
|
||||||
for c in self.configuration]
|
for c in self.configuration]
|
||||||
self.print_jobs = [CloudClusterPrintJobConstraint(**p) if isinstance(p, dict) else p
|
self.constraints = [CloudClusterPrintJobConstraint(**p) if isinstance(p, dict) else p
|
||||||
for p in self.constraints]
|
for p in self.constraints]
|
||||||
|
|
||||||
## Creates an UM3 print job output model based on this cloud cluster print job.
|
## Creates an UM3 print job output model based on this cloud cluster print job.
|
||||||
# \param printer: The output model of the printer
|
# \param printer: The output model of the printer
|
||||||
def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel:
|
def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel:
|
||||||
model = UM3PrintJobOutputModel(controller, self.uuid, self.name)
|
model = UM3PrintJobOutputModel(controller, self.uuid, self.name)
|
||||||
|
self.updateOutputModel(model)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
## Creates a new configuration model
|
||||||
|
def _createConfigurationModel(self) -> ConfigurationModel:
|
||||||
|
extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
|
||||||
|
configuration = ConfigurationModel()
|
||||||
|
configuration.setExtruderConfigurations(extruders)
|
||||||
|
return configuration
|
||||||
|
|
||||||
## Updates an UM3 print job output model based on this cloud cluster print job.
|
## Updates an UM3 print job output model based on this cloud cluster print job.
|
||||||
# \param model: The model to update.
|
# \param model: The model to update.
|
||||||
def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None:
|
def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None:
|
||||||
|
# TODO: Add `compatible_machine_families` to the cloud, than add model.setCompatibleMachineFamilies()
|
||||||
|
# TODO: Add `impediments_to_printing` to the cloud, see ClusterUM3OutputDevice._updatePrintJob
|
||||||
|
# TODO: Use model.updateConfigurationChanges, see ClusterUM3OutputDevice#_createConfigurationChanges
|
||||||
|
model.updateConfiguration(self._createConfigurationModel())
|
||||||
model.updateTimeTotal(self.time_total)
|
model.updateTimeTotal(self.time_total)
|
||||||
model.updateTimeElapsed(self.time_elapsed)
|
model.updateTimeElapsed(self.time_elapsed)
|
||||||
model.updateOwner(self.owner)
|
model.updateOwner(self.owner)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
from .CloudClusterPrinterConfiguration import CloudClusterPrinterConfiguration
|
from .CloudClusterPrinterConfiguration import CloudClusterPrinterConfiguration
|
||||||
|
@ -40,5 +41,9 @@ class CloudClusterPrinter(BaseModel):
|
||||||
model.updateType(self.machine_variant)
|
model.updateType(self.machine_variant)
|
||||||
model.updateState(self.status if self.enabled else "disabled")
|
model.updateState(self.status if self.enabled else "disabled")
|
||||||
|
|
||||||
for configuration, extruder in zip(self.configuration, model.extruders):
|
for configuration, extruder_output, extruder_config in \
|
||||||
configuration.updateOutputModel(extruder)
|
zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations):
|
||||||
|
configuration.updateOutputModel(extruder_output)
|
||||||
|
configuration.updateConfigurationModel(extruder_config)
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
|
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||||
from .CloudClusterPrinterConfigurationMaterial import CloudClusterPrinterConfigurationMaterial
|
from .CloudClusterPrinterConfigurationMaterial import CloudClusterPrinterConfigurationMaterial
|
||||||
from ...Models import BaseModel
|
from ...Models import BaseModel
|
||||||
|
@ -8,7 +12,7 @@ from ...Models import BaseModel
|
||||||
## Class representing a cloud cluster printer configuration
|
## Class representing a cloud cluster printer configuration
|
||||||
class CloudClusterPrinterConfiguration(BaseModel):
|
class CloudClusterPrinterConfiguration(BaseModel):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
self.extruder_index = None # type: str
|
self.extruder_index = None # type: int
|
||||||
self.material = None # type: CloudClusterPrinterConfigurationMaterial
|
self.material = None # type: CloudClusterPrinterConfigurationMaterial
|
||||||
self.nozzle_diameter = None # type: str
|
self.nozzle_diameter = None # type: str
|
||||||
self.print_core_id = None # type: str
|
self.print_core_id = None # type: str
|
||||||
|
@ -25,3 +29,16 @@ class CloudClusterPrinterConfiguration(BaseModel):
|
||||||
if model.activeMaterial is None or model.activeMaterial.guid != self.material.guid:
|
if model.activeMaterial is None or model.activeMaterial.guid != self.material.guid:
|
||||||
material = self.material.createOutputModel()
|
material = self.material.createOutputModel()
|
||||||
model.updateActiveMaterial(material)
|
model.updateActiveMaterial(material)
|
||||||
|
|
||||||
|
## Creates a configuration model
|
||||||
|
def createConfigurationModel(self) -> ExtruderConfigurationModel:
|
||||||
|
model = ExtruderConfigurationModel(position = self.extruder_index)
|
||||||
|
self.updateConfigurationModel(model)
|
||||||
|
return model
|
||||||
|
|
||||||
|
## Creates a configuration model
|
||||||
|
def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel:
|
||||||
|
model.setHotendID(self.print_core_id)
|
||||||
|
if self.material:
|
||||||
|
model.setMaterial(self.material.createOutputModel())
|
||||||
|
return model
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import TypeVar, Dict, Tuple, List
|
from typing import TypeVar, Dict, Tuple, List
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
U = TypeVar("U")
|
U = TypeVar("U")
|
||||||
|
|
||||||
|
@ -24,3 +27,27 @@ def findChanges(previous: Dict[str, T], received: Dict[str, U]) -> Tuple[List[T]
|
||||||
updated = [(previous[updated_id], received[updated_id]) for updated_id in updated_ids]
|
updated = [(previous[updated_id], received[updated_id]) for updated_id in updated_ids]
|
||||||
|
|
||||||
return removed, added, updated
|
return removed, added, updated
|
||||||
|
|
||||||
|
|
||||||
|
def formatTimeCompleted(time_remaining: int) -> str:
|
||||||
|
completed = datetime.now() + timedelta(seconds=time_remaining)
|
||||||
|
return "{hour:02d}:{minute:02d}".format(hour = completed.hour, minute = completed.minute)
|
||||||
|
|
||||||
|
|
||||||
|
def formatDateCompleted(time_remaining: int) -> str:
|
||||||
|
remaining = timedelta(seconds=time_remaining)
|
||||||
|
completed = datetime.now() + remaining
|
||||||
|
i18n = i18nCatalog("cura")
|
||||||
|
|
||||||
|
# If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format
|
||||||
|
if remaining.days >= 7:
|
||||||
|
return completed.strftime("%a %b ") + "{day}".format(day = completed.day)
|
||||||
|
# If finishing date is within the next week, use "Monday at HH:MM" format
|
||||||
|
elif remaining.days >= 2:
|
||||||
|
return completed.strftime("%a")
|
||||||
|
# If finishing tomorrow, use "tomorrow at HH:MM" format
|
||||||
|
elif remaining.days >= 1:
|
||||||
|
return i18n.i18nc("@info:status", "tomorrow")
|
||||||
|
# If finishing today, use "today at HH:MM" format
|
||||||
|
else:
|
||||||
|
return i18n.i18nc("@info:status", "today")
|
||||||
|
|
|
@ -25,6 +25,7 @@ from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationM
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||||
|
from plugins.UM3NetworkPrinting.src.Cloud.Utils import formatTimeCompleted, formatDateCompleted
|
||||||
|
|
||||||
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
|
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
|
||||||
from .ConfigurationChangeModel import ConfigurationChangeModel
|
from .ConfigurationChangeModel import ConfigurationChangeModel
|
||||||
|
@ -337,14 +338,12 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||||
return self._printers
|
return self._printers
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
def formatDuration(self, seconds: int) -> str:
|
def getTimeCompleted(self, time_remaining: int) -> str:
|
||||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
return formatTimeCompleted(time_remaining)
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
def getTimeCompleted(self, time_remaining: int) -> str:
|
def getDateCompleted(self, time_remaining: int) -> str:
|
||||||
current_time = time()
|
return formatDateCompleted(time_remaining)
|
||||||
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
|
||||||
return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
def getDateCompleted(self, time_remaining: int) -> str:
|
def getDateCompleted(self, time_remaining: int) -> str:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue