mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-10 23:35:07 -06:00
Restructure codebase - part 1
This commit is contained in:
parent
87517a77c2
commit
3c1b377308
46 changed files with 898 additions and 1725 deletions
659
plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py
Normal file
659
plugins/UM3NetworkPrinting/src/Network/ClusterUM3OutputDevice.py
Normal file
|
@ -0,0 +1,659 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Any, cast, Tuple, Union, Optional, Dict, List
|
||||
from time import time
|
||||
|
||||
import io # To create the correct buffers for sending data to the printer.
|
||||
import json
|
||||
import os
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously.
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Qt.Duration import Duration, DurationFormat
|
||||
from UM.Scene.SceneNode import SceneNode # For typing.
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
||||
from plugins.UM3NetworkPrinting.src.Factories.PrinterModelFactory import PrinterModelFactory
|
||||
from plugins.UM3NetworkPrinting.src.UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
|
||||
|
||||
from plugins.UM3NetworkPrinting.src.Cloud.Utils import formatTimeCompleted, formatDateCompleted
|
||||
from plugins.UM3NetworkPrinting.src.Network.ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
|
||||
from plugins.UM3NetworkPrinting.src.MeshFormatHandler import MeshFormatHandler
|
||||
from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
|
||||
from plugins.UM3NetworkPrinting.src.Models.UM3PrintJobOutputModel import UM3PrintJobOutputModel
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
from PyQt5.QtGui import QDesktopServices, QImage
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class ClusterUM3OutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||
|
||||
activeCameraUrlChanged = pyqtSignal()
|
||||
receivedPrintJobsChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id, address, properties, parent = None) -> None:
|
||||
super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
|
||||
self._api_prefix = "/cluster-api/v1/"
|
||||
|
||||
self._application = CuraApplication.getInstance()
|
||||
|
||||
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._received_print_jobs = False # type: bool
|
||||
|
||||
if PluginRegistry.getInstance() is not None:
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting")
|
||||
raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting")
|
||||
self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml")
|
||||
|
||||
# Trigger the printersChanged signal when the private signal is triggered
|
||||
self.printersChanged.connect(self._clusterPrintersChanged)
|
||||
|
||||
self._accepts_commands = True # type: bool
|
||||
|
||||
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.setPriority(3) # Make sure the output device gets selected above local file output
|
||||
self.setName(self._id)
|
||||
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
|
||||
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
|
||||
|
||||
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))
|
||||
|
||||
self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str]
|
||||
|
||||
self._finished_jobs = [] # type: List[UM3PrintJobOutputModel]
|
||||
|
||||
self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int
|
||||
|
||||
self._latest_reply_handler = None # type: Optional[QNetworkReply]
|
||||
self._sending_job = None
|
||||
|
||||
self._active_camera_url = QUrl() # type: QUrl
|
||||
|
||||
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:
|
||||
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()
|
||||
is_job_sent = True
|
||||
else: # Just immediately continue.
|
||||
self._sending_job.send("") # No specifically selected printer.
|
||||
is_job_sent = 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()
|
||||
|
||||
## Whether the printer that this output device represents supports print job actions via the local network.
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def supportsPrintJobActions(self) -> bool:
|
||||
return True
|
||||
|
||||
## 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)
|
||||
|
||||
@pyqtProperty(QUrl, notify = activeCameraUrlChanged)
|
||||
def activeCameraUrl(self) -> "QUrl":
|
||||
return self._active_camera_url
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
|
||||
if self._active_camera_url != camera_url:
|
||||
self._active_camera_url = camera_url
|
||||
self.activeCameraUrlChanged.emit()
|
||||
|
||||
def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
|
||||
if self._progress_message:
|
||||
self._progress_message.hide()
|
||||
self._compressing_gcode = False
|
||||
self._sending_gcode = False
|
||||
|
||||
## The IP address of the printer.
|
||||
@pyqtProperty(str, constant = True)
|
||||
def address(self) -> str:
|
||||
return self._address
|
||||
|
||||
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")
|
||||
|
||||
@pyqtSlot(name="openPrintJobControlPanel")
|
||||
def openPrintJobControlPanel(self) -> None:
|
||||
Logger.log("d", "Opening print job control panel...")
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
||||
|
||||
@pyqtSlot(name="openPrinterControlPanel")
|
||||
def openPrinterControlPanel(self) -> None:
|
||||
Logger.log("d", "Opening printer control panel...")
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
||||
|
||||
@pyqtProperty(bool, notify = receivedPrintJobsChanged)
|
||||
def receivedPrintJobs(self) -> bool:
|
||||
return self._received_print_jobs
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getTimeCompleted(self, time_remaining: int) -> str:
|
||||
return formatTimeCompleted(time_remaining)
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getDateCompleted(self, time_remaining: int) -> str:
|
||||
return formatDateCompleted(time_remaining)
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def formatDuration(self, seconds: int) -> str:
|
||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sendJobToTop(self, print_job_uuid: str) -> None:
|
||||
# This function is part of the output device (and not of the printjob output model) as this type of operation
|
||||
# is a modification of the cluster queue and not of the actual job.
|
||||
data = "{\"to_position\": 0}"
|
||||
self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
|
||||
# This function is part of the output device (and not of the printjob output model) as this type of operation
|
||||
# is a modification of the cluster queue and not of the actual job.
|
||||
self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def forceSendJob(self, print_job_uuid: str) -> None:
|
||||
data = "{\"force\": true}"
|
||||
self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None)
|
||||
|
||||
# Set the remote print job state.
|
||||
def setJobState(self, print_job_uuid: str, state: str) -> None:
|
||||
# We rewrite 'resume' to 'print' here because we are using the old print job action endpoints.
|
||||
action = "print" if state == "resume" else state
|
||||
data = "{\"action\": \"%s\"}" % action
|
||||
self.put("print_jobs/%s/action" % print_job_uuid, data, on_finished=None)
|
||||
|
||||
def _printJobStateChanged(self) -> None:
|
||||
username = self._getUserName()
|
||||
|
||||
if username is None:
|
||||
return # We only want to show notifications if username is set.
|
||||
|
||||
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 = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(printer_name=job.assignedPrinter.name, job_name = job.name)
|
||||
else:
|
||||
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.").format(job_name = job.name)
|
||||
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
|
||||
job_completed_message.show()
|
||||
|
||||
# Ensure UI gets updated
|
||||
self.printJobsChanged.emit()
|
||||
|
||||
# Keep a list of all completed jobs so we know if something changed next time.
|
||||
self._finished_jobs = finished_jobs
|
||||
|
||||
## Called when the connection to the cluster changes.
|
||||
def connect(self) -> None:
|
||||
super().connect()
|
||||
self.sendMaterialProfiles()
|
||||
|
||||
def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
|
||||
reply_url = reply.url().toString()
|
||||
|
||||
uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")]
|
||||
|
||||
print_job = findByKey(self._print_jobs, uuid)
|
||||
if print_job:
|
||||
image = QImage()
|
||||
image.loadFromData(reply.readAll())
|
||||
print_job.updatePreviewImage(image)
|
||||
|
||||
def _update(self) -> None:
|
||||
super()._update()
|
||||
self.get("printers/", on_finished = self._onGetPrintersDataFinished)
|
||||
self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)
|
||||
|
||||
for print_job in self._print_jobs:
|
||||
if print_job.getPreviewImage() is None:
|
||||
self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished)
|
||||
|
||||
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
|
||||
self._received_print_jobs = True
|
||||
self.receivedPrintJobsChanged.emit()
|
||||
|
||||
if not checkValidGetReply(reply):
|
||||
return
|
||||
|
||||
result = loadJsonFromReply(reply)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
print_jobs_seen = []
|
||||
job_list_changed = False
|
||||
for idx, print_job_data in enumerate(result):
|
||||
print_job = findByKey(self._print_jobs, print_job_data["uuid"])
|
||||
if print_job is None:
|
||||
print_job = self._createPrintJobModel(print_job_data)
|
||||
job_list_changed = True
|
||||
elif not job_list_changed:
|
||||
# Check if the order of the jobs has changed since the last check
|
||||
if self._print_jobs.index(print_job) != idx:
|
||||
job_list_changed = True
|
||||
|
||||
self._updatePrintJob(print_job, print_job_data)
|
||||
|
||||
if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer.
|
||||
if print_job.state in ["failed", "finished", "aborted", "none"]:
|
||||
# Print job was already completed, so don't attach it to a printer.
|
||||
printer = None
|
||||
else:
|
||||
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
|
||||
else: # The job can "reserve" a printer if some changes are required.
|
||||
printer = self._getPrinterByKey(print_job_data["assigned_to"])
|
||||
|
||||
if printer:
|
||||
printer.updateActivePrintJob(print_job)
|
||||
|
||||
print_jobs_seen.append(print_job)
|
||||
|
||||
# Check what jobs need to be removed.
|
||||
removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
|
||||
|
||||
for removed_job in removed_jobs:
|
||||
job_list_changed = job_list_changed or self._removeJob(removed_job)
|
||||
|
||||
if job_list_changed:
|
||||
# Override the old list with the new list (either because jobs were removed / added or order changed)
|
||||
self._print_jobs = print_jobs_seen
|
||||
self.printJobsChanged.emit() # Do a single emit for all print job changes.
|
||||
|
||||
def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
|
||||
if not checkValidGetReply(reply):
|
||||
return
|
||||
|
||||
result = loadJsonFromReply(reply)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
printer_list_changed = False
|
||||
printers_seen = []
|
||||
|
||||
for printer_data in result:
|
||||
printer = findByKey(self._printers, printer_data["uuid"])
|
||||
|
||||
if printer is None:
|
||||
output_controller = ClusterUM3PrinterOutputController(self)
|
||||
printer = PrinterModelFactory.createPrinter(output_controller=output_controller,
|
||||
ip_address=printer_data.get("ip_address", ""),
|
||||
extruder_count=self._number_of_extruders)
|
||||
self._printers.append(printer)
|
||||
printer_list_changed = True
|
||||
|
||||
printers_seen.append(printer)
|
||||
|
||||
self._updatePrinter(printer, printer_data)
|
||||
|
||||
removed_printers = [printer for printer in self._printers if printer not in printers_seen]
|
||||
for printer in removed_printers:
|
||||
self._removePrinter(printer)
|
||||
|
||||
if removed_printers or printer_list_changed:
|
||||
self.printersChanged.emit()
|
||||
|
||||
def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel:
|
||||
print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||
key=data["uuid"], name= data["name"])
|
||||
|
||||
configuration = PrinterConfigurationModel()
|
||||
extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
|
||||
for index in range(0, self._number_of_extruders):
|
||||
try:
|
||||
extruder_data = data["configuration"][index]
|
||||
except IndexError:
|
||||
continue
|
||||
extruder = extruders[int(data["configuration"][index]["extruder_index"])]
|
||||
extruder.setHotendID(extruder_data.get("print_core_id", ""))
|
||||
extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))
|
||||
|
||||
configuration.setExtruderConfigurations(extruders)
|
||||
configuration.setPrinterType(data.get("machine_variant", ""))
|
||||
print_job.updateConfiguration(configuration)
|
||||
print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
|
||||
print_job.stateChanged.connect(self._printJobStateChanged)
|
||||
return print_job
|
||||
|
||||
def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None:
|
||||
print_job.updateTimeTotal(data["time_total"])
|
||||
print_job.updateTimeElapsed(data["time_elapsed"])
|
||||
impediments_to_printing = data.get("impediments_to_printing", [])
|
||||
print_job.updateOwner(data["owner"])
|
||||
|
||||
status_set_by_impediment = False
|
||||
for impediment in impediments_to_printing:
|
||||
if impediment["severity"] == "UNFIXABLE":
|
||||
status_set_by_impediment = True
|
||||
print_job.updateState("error")
|
||||
break
|
||||
|
||||
if not status_set_by_impediment:
|
||||
print_job.updateState(data["status"])
|
||||
|
||||
configuration_changes = PrinterModelFactory.createConfigurationChanges(data.get("configuration_changes_required"))
|
||||
print_job.updateConfigurationChanges(configuration_changes)
|
||||
|
||||
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)
|
||||
|
||||
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
|
||||
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
|
||||
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
|
||||
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
|
||||
|
||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
|
||||
if not definitions:
|
||||
Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
|
||||
return
|
||||
|
||||
machine_definition = definitions[0]
|
||||
|
||||
printer.updateName(data["friendly_name"])
|
||||
printer.updateKey(data["uuid"])
|
||||
printer.updateType(data["machine_variant"])
|
||||
|
||||
if data["status"] != "unreachable":
|
||||
self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(data["ip_address"],
|
||||
name = data["friendly_name"],
|
||||
machine_type = data["machine_variant"])
|
||||
|
||||
# Do not store the build plate information that comes from connect if the current printer has not build plate information
|
||||
if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
|
||||
printer.updateBuildplate(data["build_plate"]["type"])
|
||||
if not data["enabled"]:
|
||||
printer.updateState("disabled")
|
||||
else:
|
||||
printer.updateState(data["status"])
|
||||
|
||||
for index in range(0, self._number_of_extruders):
|
||||
extruder = printer.extruders[index]
|
||||
try:
|
||||
extruder_data = data["configuration"][index]
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
extruder.updateHotendID(extruder_data.get("print_core_id", ""))
|
||||
|
||||
material_data = extruder_data["material"]
|
||||
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
|
||||
material = self._createMaterialOutputModel(material_data)
|
||||
extruder.updateActiveMaterial(material)
|
||||
|
||||
def _removeJob(self, job: UM3PrintJobOutputModel) -> bool:
|
||||
if job not in self._print_jobs:
|
||||
return False
|
||||
|
||||
if job.assignedPrinter:
|
||||
job.assignedPrinter.updateActivePrintJob(None)
|
||||
job.stateChanged.disconnect(self._printJobStateChanged)
|
||||
self._print_jobs.remove(job)
|
||||
|
||||
return True
|
||||
|
||||
def _removePrinter(self, printer: PrinterOutputModel) -> None:
|
||||
self._printers.remove(printer)
|
||||
if self._active_printer == printer:
|
||||
self._active_printer = None
|
||||
self.activePrinterChanged.emit()
|
||||
|
||||
## Sync the material profiles in Cura with the printer.
|
||||
#
|
||||
# This gets called when connecting to a printer as well as when sending a
|
||||
# print.
|
||||
def sendMaterialProfiles(self) -> None:
|
||||
job = SendMaterialJob(device = self)
|
||||
job.run()
|
||||
|
||||
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
|
||||
try:
|
||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.logException("w", "Unable to decode JSON from reply.")
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
def checkValidGetReply(reply: QNetworkReply) -> bool:
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
|
||||
if status_code != 200:
|
||||
Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def findByKey(lst: List[Union[UM3PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[UM3PrintJobOutputModel]:
|
||||
for item in lst:
|
||||
if item.key == key:
|
||||
return item
|
||||
return None
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
|
||||
|
||||
class ClusterUM3PrinterOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
self.can_pre_heat_bed = False
|
||||
self.can_pre_heat_hotends = False
|
||||
self.can_control_manually = False
|
||||
self.can_send_raw_gcode = False
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
|
||||
self._output_device.setJobState(job.key, state)
|
|
@ -0,0 +1,13 @@
|
|||
from typing import Optional, Callable
|
||||
|
||||
|
||||
## Represents a request for adding a manual printer. It has the following fields:
|
||||
# - address: The string of the (IP) address of the manual printer
|
||||
# - callback: (Optional) Once the HTTP request to the printer to get printer information is done, whether successful
|
||||
# or not, this callback will be invoked to notify about the result. The callback must have a signature of
|
||||
# func(success: bool, address: str) -> None
|
||||
class ManualPrinterRequest:
|
||||
|
||||
def __init__(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
|
||||
self.address = address
|
||||
self.callback = callback
|
23
plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py
Normal file
23
plugins/UM3NetworkPrinting/src/Network/NetworkApiClient.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
|
||||
## The network API client is responsible for handling requests and responses to printer over the local network (LAN).
|
||||
class NetworkApiClient:
|
||||
|
||||
API_PREFIX = "/cluster-api/v1/"
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def getPrinters(self):
|
||||
pass
|
||||
|
||||
def getPrintJobs(self):
|
||||
pass
|
||||
|
||||
def requestPrint(self):
|
||||
pass
|
||||
|
||||
def doPrintJobAction(self):
|
||||
pass
|
|
@ -0,0 +1,425 @@
|
|||
from queue import Queue
|
||||
from threading import Thread, Event
|
||||
from time import time
|
||||
from typing import Dict, Optional, Callable, List
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
||||
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
from UM.Version import Version
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from plugins.UM3NetworkPrinting.src.Network.ClusterUM3OutputDevice import ClusterUM3OutputDevice
|
||||
from plugins.UM3NetworkPrinting.src.Network.ManualPrinterRequest import ManualPrinterRequest
|
||||
|
||||
|
||||
## The NetworkOutputDeviceManager is responsible for discovering and managing local networked clusters.
|
||||
class NetworkOutputDeviceManager:
|
||||
|
||||
PRINTER_API_VERSION = "1"
|
||||
PRINTER_API_PREFIX = "/api/v" + PRINTER_API_VERSION
|
||||
|
||||
CLUSTER_API_VERSION = "1"
|
||||
CLUSTER_API_PREFIX = "/cluster-api/v" + CLUSTER_API_VERSION
|
||||
|
||||
ZERO_CONF_NAME = u"_ultimaker._tcp.local."
|
||||
|
||||
MANUAL_DEVICES_PREFERENCE_KEY = "um3networkprinting/manual_instances"
|
||||
|
||||
MIN_SUPPORTED_CLUSTER_VERSION = Version("4.0.0")
|
||||
|
||||
discoveredDevicesChanged = Signal()
|
||||
addedNetworkCluster = Signal()
|
||||
removedNetworkCluster = Signal()
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Persistent dict containing the networked clusters.
|
||||
self._discovered_devices = {} # type: Dict[str, ClusterUM3OutputDevice]
|
||||
self._output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
self._anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
||||
|
||||
self._zero_conf = None # type: Optional[Zeroconf]
|
||||
self._zero_conf_browser = None # type: Optional[ServiceBrowser]
|
||||
self._service_changed_request_queue = None # type: Optional[Queue]
|
||||
self._service_changed_request_event = None # type: Optional[Event]
|
||||
self._service_changed_request_thread = None # type: Optional[Thread]
|
||||
|
||||
# Persistent dict containing manually connected clusters.
|
||||
self._manual_instances = {} # type: Dict[str, ManualPrinterRequest]
|
||||
self._last_manual_entry_key = None # type: Optional[str]
|
||||
|
||||
# Hook up the signals for discovery.
|
||||
self.addedNetworkCluster.connect(self._onAddDevice)
|
||||
self.removedNetworkCluster.connect(self._onRemoveDevice)
|
||||
|
||||
# # Get all discovered devices in the local network.
|
||||
# def getDiscoveredDevices(self) -> Dict[str, ClusterUM3OutputDevice]:
|
||||
# return self._discovered_devices
|
||||
|
||||
# ## Get the key of the last manually added device.
|
||||
# def getLastManualDevice(self) -> str:
|
||||
# return self._last_manual_entry_key
|
||||
|
||||
# ## Reset the last manually added device key.
|
||||
# def resetLastManualDevice(self) -> None:
|
||||
# self._last_manual_entry_key = ""
|
||||
|
||||
## Force reset all network device connections.
|
||||
def refreshConnections(self):
|
||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
||||
um_network_key = active_machine.getMetaDataEntry("um_network_key")
|
||||
|
||||
for key in self._discovered_devices:
|
||||
if key == um_network_key:
|
||||
if not self._discovered_devices[key].isConnected():
|
||||
Logger.log("d", "Attempting to connect with [%s]" % key)
|
||||
# It should already be set, but if it actually connects we know for sure it's supported!
|
||||
active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value)
|
||||
self._discovered_devices[key].connect()
|
||||
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
|
||||
else:
|
||||
self._onDeviceConnectionStateChanged(key)
|
||||
else:
|
||||
if self._discovered_devices[key].isConnected():
|
||||
Logger.log("d", "Attempting to close connection with [%s]" % key)
|
||||
self._discovered_devices[key].close()
|
||||
self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
|
||||
|
||||
## Start the network discovery.
|
||||
def start(self):
|
||||
# The ZeroConf service changed requests are handled in a separate thread so we don't block the UI.
|
||||
# We can also re-schedule the requests when they fail to get detailed service info.
|
||||
# Any new or re-reschedule requests will be appended to the request queue and the thread will process them.
|
||||
self._service_changed_request_queue = Queue()
|
||||
self._service_changed_request_event = Event()
|
||||
self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
|
||||
self._service_changed_request_thread.start()
|
||||
|
||||
# Start network discovery.
|
||||
self.stop()
|
||||
self._zero_conf = Zeroconf()
|
||||
self._zero_conf_browser = ServiceBrowser(self._zero_conf, self.ZERO_CONF_NAME, [
|
||||
self._appendServiceChangedRequest
|
||||
])
|
||||
|
||||
# Load all manual devices.
|
||||
self._manual_instances = self._getStoredManualInstances()
|
||||
for address in self._manual_instances:
|
||||
if address:
|
||||
self.addManualDevice(address)
|
||||
# TODO: self.resetLastManualDevice()
|
||||
|
||||
## Stop network discovery and clean up discovered devices.
|
||||
def stop(self):
|
||||
# Cleanup ZeroConf resources.
|
||||
if self._zero_conf is not None:
|
||||
self._zero_conf.close()
|
||||
self._zero_conf = None
|
||||
if self._zero_conf_browser is not None:
|
||||
self._zero_conf_browser.cancel()
|
||||
self._zero_conf_browser = None
|
||||
|
||||
# Cleanup all manual devices.
|
||||
for instance_name in list(self._discovered_devices):
|
||||
self._onRemoveDevice(instance_name)
|
||||
|
||||
## Add a networked printer manually by address.
|
||||
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
|
||||
self._manual_instances[address] = ManualPrinterRequest(address, callback=callback)
|
||||
new_manual_devices = ",".join(self._manual_instances.keys())
|
||||
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_manual_devices)
|
||||
|
||||
key = f"manual:{address}"
|
||||
if key not in self._discovered_devices:
|
||||
self._onAddDevice(key, address, {
|
||||
b"name": address.encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"incomplete": b"true",
|
||||
b"temporary": b"true"
|
||||
})
|
||||
|
||||
self._last_manual_entry_key = key
|
||||
response_callback = lambda status_code, response: self._onCheckManualDeviceResponse(status_code, address)
|
||||
self._checkManualDevice(address, response_callback)
|
||||
|
||||
## Remove a manually added networked printer.
|
||||
def removeManualDevice(self, key: str, address: Optional[str] = None) -> None:
|
||||
if key not in self._discovered_devices and address is not None:
|
||||
key = f"manual:{address}"
|
||||
|
||||
if key in self._discovered_devices:
|
||||
if not address:
|
||||
address = self._discovered_devices[key].ipAddress
|
||||
self._onRemoveDevice(key)
|
||||
# TODO: self.resetLastManualDevice()
|
||||
|
||||
if address in self._manual_instances:
|
||||
manual_printer_request = self._manual_instances.pop(address)
|
||||
new_manual_devices = ",".join(self._manual_instances.keys())
|
||||
CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY,
|
||||
new_manual_devices)
|
||||
if manual_printer_request.callback is not None:
|
||||
CuraApplication.getInstance().callLater(manual_printer_request.callback, False, address)
|
||||
|
||||
## Checks if a networked printer exists at the given address.
|
||||
# If the printer responds it will replace the preliminary printer created from the stored manual instances.
|
||||
def _checkManualDevice(self, address: str, on_finished: Callable) -> None:
|
||||
Logger.log("d", "checking manual device: {}".format(address))
|
||||
url = QUrl(f"http://{address}/{self.PRINTER_API_PREFIX}/system")
|
||||
request = QNetworkRequest(url)
|
||||
reply = self._network_manager.get(request)
|
||||
self._addCallback(reply, on_finished)
|
||||
|
||||
## Callback for when a manual device check request was responded to.
|
||||
def _onCheckManualDeviceResponse(self, status_code: int, address: str) -> None:
|
||||
Logger.log("d", "manual device check response: {} {}".format(status_code, address))
|
||||
if address in self._manual_instances:
|
||||
callback = self._manual_instances[address].callback
|
||||
if callback:
|
||||
CuraApplication.getInstance().callLater(callback, status_code == 200, address)
|
||||
|
||||
## Returns a dict of printer BOM numbers to machine types.
|
||||
# These numbers are available in the machine definition already so we just search for them here.
|
||||
@staticmethod
|
||||
def _getPrinterTypeIdentifiers() -> Dict[str, str]:
|
||||
container_registry = CuraApplication.getInstance().getContainerRegistry()
|
||||
ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.")
|
||||
found_machine_type_identifiers = {} # type: Dict[str, str]
|
||||
for machine in ultimaker_machines:
|
||||
machine_bom_number = machine.get("firmware_update_info", {}).get("id", None)
|
||||
machine_type = machine.get("id", None)
|
||||
if machine_bom_number and machine_type:
|
||||
found_machine_type_identifiers[str(machine_bom_number)] = machine_type
|
||||
return found_machine_type_identifiers
|
||||
|
||||
## Add a new device.
|
||||
def _onAddDevice(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None:
|
||||
cluster_size = int(properties.get(b"cluster_size", -1))
|
||||
printer_type = properties.get(b"machine", b"").decode("utf-8")
|
||||
printer_type_identifiers = self._getPrinterTypeIdentifiers()
|
||||
|
||||
# Detect the machine type based on the BOM number that is sent over the network.
|
||||
for bom, p_type in printer_type_identifiers.items():
|
||||
if printer_type.startswith(bom):
|
||||
properties[b"printer_type"] = bytes(p_type, encoding="utf8")
|
||||
break
|
||||
else:
|
||||
properties[b"printer_type"] = b"Unknown"
|
||||
|
||||
# We no longer support legacy devices, so check that here.
|
||||
if cluster_size == -1:
|
||||
return
|
||||
|
||||
device = ClusterUM3OutputDevice(key, address, properties)
|
||||
|
||||
CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter(
|
||||
ip_address=address,
|
||||
key=device.getId(),
|
||||
name=properties[b"name"].decode("utf-8"),
|
||||
create_callback=self._createMachineFromDiscoveredPrinter,
|
||||
machine_type=properties[b"printer_type"].decode("utf-8"),
|
||||
device=device
|
||||
)
|
||||
|
||||
self._discovered_devices[device.getId()] = device
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||
# Ensure that the configured connection type is set.
|
||||
global_container_stack.addConfiguredConnectionType(device.connectionType.value)
|
||||
device.connect()
|
||||
device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
|
||||
|
||||
## Remove a device.
|
||||
def _onRemoveDevice(self, device_id: str) -> None:
|
||||
device = self._discovered_devices.pop(device_id, None)
|
||||
if device:
|
||||
if device.isConnected():
|
||||
device.disconnect()
|
||||
try:
|
||||
device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
|
||||
except TypeError:
|
||||
# Disconnect already happened.
|
||||
pass
|
||||
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address)
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
## Appends a service changed request so later the handling thread will pick it up and processes it.
|
||||
def _appendServiceChangedRequest(self, zeroconf: Zeroconf, service_type, name: str,
|
||||
state_change: ServiceStateChange) -> None:
|
||||
item = (zeroconf, service_type, name, state_change)
|
||||
self._service_changed_request_queue.put(item)
|
||||
self._service_changed_request_event.set()
|
||||
|
||||
def _handleOnServiceChangedRequests(self) -> None:
|
||||
while True:
|
||||
# Wait for the event to be set
|
||||
self._service_changed_request_event.wait(timeout=5.0)
|
||||
|
||||
# Stop if the application is shutting down
|
||||
if CuraApplication.getInstance().isShuttingDown():
|
||||
return
|
||||
|
||||
self._service_changed_request_event.clear()
|
||||
|
||||
# Handle all pending requests
|
||||
reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled
|
||||
while not self._service_changed_request_queue.empty():
|
||||
request = self._service_changed_request_queue.get()
|
||||
zeroconf, service_type, name, state_change = request
|
||||
try:
|
||||
result = self._onServiceChanged(zeroconf, service_type, name, state_change)
|
||||
if not result:
|
||||
reschedule_requests.append(request)
|
||||
except Exception:
|
||||
Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
|
||||
service_type, name)
|
||||
reschedule_requests.append(request)
|
||||
|
||||
# Re-schedule the failed requests if any
|
||||
if reschedule_requests:
|
||||
for request in reschedule_requests:
|
||||
self._service_changed_request_queue.put(request)
|
||||
|
||||
## Callback handler for when the connection state of a networked device has changed.
|
||||
def _onDeviceConnectionStateChanged(self, key: str) -> None:
|
||||
if key not in self._discovered_devices:
|
||||
return
|
||||
|
||||
if self._discovered_devices[key].isConnected():
|
||||
um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
|
||||
if key != um_network_key:
|
||||
return
|
||||
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
|
||||
# TODO: self.checkCloudFlowIsPossible(None)
|
||||
else:
|
||||
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(key)
|
||||
|
||||
## Handler for zeroConf detection.
|
||||
# Return True or False indicating if the process succeeded.
|
||||
# Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread.
|
||||
def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange
|
||||
) -> bool:
|
||||
if state_change == ServiceStateChange.Added:
|
||||
return self._onServiceAdded(zero_conf, service_type, name)
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
return self._onServiceRemoved(name)
|
||||
return True
|
||||
|
||||
## Handler for when a ZeroConf service was added.
|
||||
def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool:
|
||||
# First try getting info from zero-conf cache
|
||||
info = ServiceInfo(service_type, name, properties={})
|
||||
for record in zero_conf.cache.entries_with_name(name.lower()):
|
||||
info.update_record(zero_conf, time(), record)
|
||||
|
||||
for record in zero_conf.cache.entries_with_name(info.server):
|
||||
info.update_record(zero_conf, time(), record)
|
||||
if info.address:
|
||||
break
|
||||
|
||||
# Request more data if info is not complete
|
||||
if not info.address:
|
||||
info = zero_conf.get_service_info(service_type, name)
|
||||
|
||||
if info:
|
||||
type_of_device = info.properties.get(b"type", None)
|
||||
if type_of_device:
|
||||
if type_of_device == b"printer":
|
||||
address = '.'.join(map(lambda n: str(n), info.address))
|
||||
self.addedNetworkCluster.emit(str(name), address, info.properties)
|
||||
else:
|
||||
Logger.log("w",
|
||||
"The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
|
||||
else:
|
||||
Logger.log("w", "Could not get information about %s" % name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
## Handler for when a ZeroConf service was removed.
|
||||
def _onServiceRemoved(self, name: str) -> bool:
|
||||
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||
self.removedNetworkCluster.emit(str(name))
|
||||
return True
|
||||
|
||||
def _associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None:
|
||||
if not printer_device:
|
||||
return
|
||||
|
||||
Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key)
|
||||
|
||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||
global_container_stack = machine_manager.activeMachine
|
||||
if not global_container_stack:
|
||||
return
|
||||
|
||||
for machine in machine_manager.getMachinesInGroup(global_container_stack.getMetaDataEntry("group_id")):
|
||||
machine.setMetaDataEntry("um_network_key", printer_device.key)
|
||||
machine.setMetaDataEntry("group_name", printer_device.name)
|
||||
|
||||
# Delete old authentication data.
|
||||
Logger.log("d", "Removing old authentication id %s for device %s",
|
||||
global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key)
|
||||
|
||||
machine.removeMetaDataEntry("network_authentication_id")
|
||||
machine.removeMetaDataEntry("network_authentication_key")
|
||||
|
||||
# Ensure that these containers do know that they are configured for network connection
|
||||
machine.addConfiguredConnectionType(printer_device.connectionType.value)
|
||||
|
||||
self.refreshConnections()
|
||||
|
||||
## Create a machine instance based on the discovered network printer.
|
||||
def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
|
||||
discovered_device = self._discovered_devices.get(key)
|
||||
if discovered_device is None:
|
||||
Logger.log("e", "Could not find discovered device with key [%s]", key)
|
||||
return
|
||||
|
||||
group_name = discovered_device.getProperty("name")
|
||||
machine_type_id = discovered_device.getProperty("printer_type")
|
||||
Logger.log("i", "Creating machine from network device with key = [%s], group name = [%s], printer type = [%s]",
|
||||
key, group_name, machine_type_id)
|
||||
|
||||
CuraApplication.getInstance().getMachineManager().addMachine(machine_type_id, group_name)
|
||||
# connect the new machine to that network printer
|
||||
self._associateActiveMachineWithPrinterDevice(discovered_device)
|
||||
# ensure that the connection states are refreshed.
|
||||
self.refreshConnections()
|
||||
|
||||
## Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
# The callback is added to the 'finished' signal of the reply.
|
||||
# \param reply: The reply that should be listened to.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
def _addCallback(self, reply: QNetworkReply, on_finished: Callable) -> None:
|
||||
def parse() -> None:
|
||||
# Don't try to parse the reply if we didn't get one
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
|
||||
return
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
response = bytes(reply.readAll()).decode()
|
||||
self._anti_gc_callbacks.remove(parse)
|
||||
on_finished(int(status_code), response)
|
||||
return
|
||||
self._anti_gc_callbacks.append(parse)
|
||||
reply.finished.connect(parse)
|
||||
|
||||
## Load the user-configured manual devices from Cura preferences.
|
||||
def _getStoredManualInstances(self) -> Dict[str, ManualPrinterRequest]:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "")
|
||||
manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",")
|
||||
return {address: ManualPrinterRequest(address) for address in manual_instances}
|
0
plugins/UM3NetworkPrinting/src/Network/__init__.py
Normal file
0
plugins/UM3NetworkPrinting/src/Network/__init__.py
Normal file
Loading…
Add table
Add a link
Reference in a new issue