mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-09 06:45:09 -06:00
Merge pull request #2974 from Ultimaker/network_rewrite
Network rewrite
This commit is contained in:
commit
ade86ebc45
61 changed files with 4649 additions and 5264 deletions
|
@ -14,60 +14,122 @@ class MonitorStage(CuraStage):
|
|||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._setComponents)
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
# Update the status icon when the output device is changed
|
||||
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setComponents(self):
|
||||
self._setMainOverlay()
|
||||
self._setSidebar()
|
||||
self._setIconSource()
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
if self._active_print_job:
|
||||
self._active_print_job.stateChanged.disconnect(self._updateIconSource)
|
||||
self._active_print_job = print_job
|
||||
if self._active_print_job:
|
||||
self._active_print_job.stateChanged.connect(self._updateIconSource)
|
||||
|
||||
def _setMainOverlay(self):
|
||||
# Ensure that the right icon source is returned.
|
||||
self._updateIconSource()
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
# Ensure that the right icon source is returned.
|
||||
self._updateIconSource()
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
|
||||
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
# Force an update of the icon source
|
||||
self._updateIconSource()
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
self._onOutputDevicesChanged()
|
||||
self._updateMainOverlay()
|
||||
self._updateSidebar()
|
||||
self._updateIconSource()
|
||||
|
||||
def _updateMainOverlay(self):
|
||||
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
|
||||
def _setSidebar(self):
|
||||
def _updateSidebar(self):
|
||||
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
|
||||
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
|
||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||
|
||||
def _setIconSource(self):
|
||||
def _updateIconSource(self):
|
||||
if Application.getInstance().getTheme() is not None:
|
||||
icon_name = self._getActiveOutputDeviceStatusIcon()
|
||||
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
|
||||
|
||||
## Find the correct status icon depending on the active output device state
|
||||
def _getActiveOutputDeviceStatusIcon(self):
|
||||
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
|
||||
|
||||
if not output_device:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
try:
|
||||
output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
except IndexError:
|
||||
return "tab_status_unknown"
|
||||
|
||||
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
|
||||
if not output_device.acceptsCommands:
|
||||
return "tab_status_unknown"
|
||||
|
||||
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
|
||||
return "tab_status_unknown"
|
||||
|
||||
# TODO: refactor to use enum instead of hardcoded strings?
|
||||
if output_device.printerState == "maintenance":
|
||||
return "tab_status_busy"
|
||||
|
||||
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
|
||||
return "tab_status_busy"
|
||||
|
||||
if output_device.jobState == "wait_cleanup":
|
||||
return "tab_status_finished"
|
||||
|
||||
if output_device.jobState in ["ready", ""]:
|
||||
if output_device.activePrinter is None:
|
||||
return "tab_status_connected"
|
||||
|
||||
if output_device.jobState == "paused":
|
||||
# TODO: refactor to use enum instead of hardcoded strings?
|
||||
if output_device.activePrinter.state == "maintenance":
|
||||
return "tab_status_busy"
|
||||
|
||||
if output_device.activePrinter.activePrintJob is None:
|
||||
return "tab_status_connected"
|
||||
|
||||
if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
|
||||
return "tab_status_busy"
|
||||
|
||||
if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
|
||||
return "tab_status_finished"
|
||||
|
||||
if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
|
||||
return "tab_status_connected"
|
||||
|
||||
if output_device.activePrinter.activePrintJob.state == "paused":
|
||||
return "tab_status_paused"
|
||||
|
||||
if output_device.jobState == "error":
|
||||
if output_device.activePrinter.activePrintJob.state == "error":
|
||||
return "tab_status_stopped"
|
||||
|
||||
return "tab_status_unknown"
|
||||
|
|
|
@ -10,13 +10,12 @@ Component
|
|||
{
|
||||
id: base
|
||||
property var manager: Cura.MachineManager.printerOutputDevices[0]
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("viewport_background")
|
||||
|
||||
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
|
||||
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
|
||||
|
||||
visible: manager != null
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("viewport_background")
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
|
@ -97,7 +96,7 @@ Component
|
|||
}
|
||||
Label
|
||||
{
|
||||
text: manager.numJobsPrinting
|
||||
text: manager.activePrintJobs.length
|
||||
font: UM.Theme.getFont("small")
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
@ -114,7 +113,7 @@ Component
|
|||
}
|
||||
Label
|
||||
{
|
||||
text: manager.numJobsQueued
|
||||
text: manager.queuedPrintJobs.length
|
||||
font: UM.Theme.getFont("small")
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@ Component
|
|||
width: maximumWidth
|
||||
height: maximumHeight
|
||||
color: UM.Theme.getColor("viewport_background")
|
||||
|
||||
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
|
||||
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
|
||||
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
|
@ -33,9 +33,9 @@ Component
|
|||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
text: OutputDevice.connectedPrinters.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
|
||||
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
|
||||
|
||||
visible: OutputDevice.connectedPrinters.length == 0
|
||||
visible: OutputDevice.printers.length == 0
|
||||
}
|
||||
|
||||
Item
|
||||
|
@ -46,7 +46,7 @@ Component
|
|||
|
||||
width: Math.min(800 * screenScaleFactor, maximumWidth)
|
||||
height: children.height
|
||||
visible: OutputDevice.connectedPrinters.length != 0
|
||||
visible: OutputDevice.printers.length != 0
|
||||
|
||||
Label
|
||||
{
|
||||
|
@ -62,7 +62,6 @@ Component
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
ScrollView
|
||||
{
|
||||
id: printerScrollView
|
||||
|
@ -79,7 +78,7 @@ Component
|
|||
anchors.fill: parent
|
||||
spacing: -UM.Theme.getSize("default_lining").height
|
||||
|
||||
model: OutputDevice.connectedPrinters
|
||||
model: OutputDevice.printers
|
||||
|
||||
delegate: PrinterInfoBlock
|
||||
{
|
||||
|
@ -95,7 +94,7 @@ Component
|
|||
|
||||
PrinterVideoStream
|
||||
{
|
||||
visible: OutputDevice.selectedPrinterName != ""
|
||||
visible: OutputDevice.activePrinter != null
|
||||
anchors.fill:parent
|
||||
}
|
||||
}
|
||||
|
|
429
plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
Normal file
429
plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
Normal file
|
@ -0,0 +1,429 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Application import Application
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
from UM.Qt.Duration import Duration, DurationFormat
|
||||
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from cura.PrinterOutput.NetworkCamera import NetworkCamera
|
||||
|
||||
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
||||
|
||||
from time import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||
printJobsChanged = pyqtSignal()
|
||||
activePrinterChanged = pyqtSignal()
|
||||
|
||||
# This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
|
||||
# Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
|
||||
clusterPrintersChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id, address, properties, parent = None):
|
||||
super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
|
||||
self._api_prefix = "/cluster-api/v1/"
|
||||
|
||||
self._number_of_extruders = 2
|
||||
|
||||
self._print_jobs = []
|
||||
|
||||
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
|
||||
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
|
||||
|
||||
# See comments about this hack with the clusterPrintersChanged signal
|
||||
self.printersChanged.connect(self.clusterPrintersChanged)
|
||||
|
||||
self._accepts_commands = True
|
||||
|
||||
# Cluster does not have authentication, so default to authenticated
|
||||
self._authentication_state = AuthState.Authenticated
|
||||
|
||||
self._error_message = None
|
||||
self._progress_message = None
|
||||
|
||||
self._active_printer = None # type: Optional[PrinterOutputModel]
|
||||
|
||||
self._printer_selection_dialog = None
|
||||
|
||||
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._printer_uuid_to_unique_name_mapping = {}
|
||||
|
||||
self._finished_jobs = []
|
||||
|
||||
self._cluster_size = int(properties.get(b"cluster_size", 0))
|
||||
|
||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
|
||||
# Notify the UI that a switch to the print monitor should happen
|
||||
Application.getInstance().getController().setActiveStage("MonitorStage")
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", [])
|
||||
if not self._gcode:
|
||||
# Unable to find g-code. Nothing to send
|
||||
return
|
||||
|
||||
if len(self._printers) > 1:
|
||||
self._spawnPrinterSelectionDialog()
|
||||
else:
|
||||
self.sendPrintJob()
|
||||
|
||||
def _spawnPrinterSelectionDialog(self):
|
||||
if self._printer_selection_dialog is None:
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
|
||||
self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
|
||||
if self._printer_selection_dialog is not None:
|
||||
self._printer_selection_dialog.show()
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def clusterSize(self):
|
||||
return self._cluster_size
|
||||
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(str)
|
||||
def sendPrintJob(self, target_printer = ""):
|
||||
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()
|
||||
return
|
||||
|
||||
self._sending_gcode = True
|
||||
|
||||
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
|
||||
i18n_catalog.i18nc("@info:title", "Sending Data"))
|
||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
|
||||
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
|
||||
self._progress_message.show()
|
||||
|
||||
compressed_gcode = self._compressGCode()
|
||||
if compressed_gcode is None:
|
||||
# Abort was called.
|
||||
return
|
||||
|
||||
parts = []
|
||||
|
||||
# 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 = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
|
||||
|
||||
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
|
||||
|
||||
self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
|
||||
|
||||
@pyqtProperty(QObject, notify=activePrinterChanged)
|
||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
||||
return self._active_printer
|
||||
|
||||
@pyqtSlot(QObject)
|
||||
def setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer and self._active_printer.camera:
|
||||
self._active_printer.camera.stop()
|
||||
self._active_printer = printer
|
||||
self.activePrinterChanged.emit()
|
||||
|
||||
def _onPostPrintJobFinished(self, reply):
|
||||
self._progress_message.hide()
|
||||
self._compressing_gcode = False
|
||||
self._sending_gcode = False
|
||||
|
||||
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
|
||||
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 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)
|
||||
else:
|
||||
self._progress_message.setProgress(0)
|
||||
self._progress_message.hide()
|
||||
|
||||
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
|
||||
if action_id == "Abort":
|
||||
Logger.log("d", "User aborted sending print to remote.")
|
||||
self._progress_message.hide()
|
||||
self._compressing_gcode = False
|
||||
self._sending_gcode = False
|
||||
Application.getInstance().getController().setActiveStage("PrepareStage")
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrintJobControlPanel(self):
|
||||
Logger.log("d", "Opening print job control panel...")
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrinterControlPanel(self):
|
||||
Logger.log("d", "Opening printer control panel...")
|
||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||
def printJobs(self):
|
||||
return self._print_jobs
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||
def queuedPrintJobs(self):
|
||||
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None]
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||
def activePrintJobs(self):
|
||||
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None]
|
||||
|
||||
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
|
||||
def connectedPrintersTypeCount(self):
|
||||
printer_count = {}
|
||||
for printer in self._printers:
|
||||
if printer.type in printer_count:
|
||||
printer_count[printer.type] += 1
|
||||
else:
|
||||
printer_count[printer.type] = 1
|
||||
result = []
|
||||
for machine_type in printer_count:
|
||||
result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
|
||||
return result
|
||||
|
||||
@pyqtSlot(int, result=str)
|
||||
def formatDuration(self, seconds):
|
||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
||||
|
||||
@pyqtSlot(int, result=str)
|
||||
def getTimeCompleted(self, time_remaining):
|
||||
current_time = time()
|
||||
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)
|
||||
def getDateCompleted(self, time_remaining):
|
||||
current_time = time()
|
||||
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
|
||||
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
|
||||
|
||||
def _printJobStateChanged(self):
|
||||
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:
|
||||
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))
|
||||
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
|
||||
|
||||
def _update(self):
|
||||
if not super()._update():
|
||||
return
|
||||
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
|
||||
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
|
||||
|
||||
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
|
||||
if not checkValidGetReply(reply):
|
||||
return
|
||||
|
||||
result = loadJsonFromReply(reply)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
print_jobs_seen = []
|
||||
job_list_changed = False
|
||||
for print_job_data in 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
|
||||
|
||||
self._updatePrintJob(print_job, print_job_data)
|
||||
|
||||
if print_job.state != "queued": # Print job should be assigned to a printer.
|
||||
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 |= self._removeJob(removed_job)
|
||||
|
||||
if job_list_changed:
|
||||
self.printJobsChanged.emit() # Do a single emit for all print job changes.
|
||||
|
||||
def _onGetPrintersDataFinished(self, reply: QNetworkReply):
|
||||
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:
|
||||
printer = self._createPrinterModel(printer_data)
|
||||
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 _createPrinterModel(self, data):
|
||||
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||
number_of_extruders=self._number_of_extruders)
|
||||
printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
|
||||
self._printers.append(printer)
|
||||
return printer
|
||||
|
||||
def _createPrintJobModel(self, data):
|
||||
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
||||
key=data["uuid"], name= data["name"])
|
||||
print_job.stateChanged.connect(self._printJobStateChanged)
|
||||
self._print_jobs.append(print_job)
|
||||
return print_job
|
||||
|
||||
def _updatePrintJob(self, print_job, data):
|
||||
print_job.updateTimeTotal(data["time_total"])
|
||||
print_job.updateTimeElapsed(data["time_elapsed"])
|
||||
print_job.updateState(data["status"])
|
||||
print_job.updateOwner(data["owner"])
|
||||
|
||||
def _updatePrinter(self, printer, data):
|
||||
# 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"]
|
||||
|
||||
printer.updateName(data["friendly_name"])
|
||||
printer.updateKey(data["uuid"])
|
||||
printer.updateType(data["machine_variant"])
|
||||
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"]:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
|
||||
GUID=material_data["guid"])
|
||||
if containers:
|
||||
color = containers[0].getMetaDataEntry("color_code")
|
||||
brand = containers[0].getMetaDataEntry("brand")
|
||||
material_type = containers[0].getMetaDataEntry("material")
|
||||
name = containers[0].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 = "Unknown"
|
||||
|
||||
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
|
||||
brand=brand, color=color, name=name)
|
||||
extruder.updateActiveMaterial(material)
|
||||
|
||||
def _removeJob(self, job):
|
||||
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):
|
||||
self._printers.remove(printer)
|
||||
if self._active_printer == printer:
|
||||
self._active_printer = None
|
||||
self.activePrinterChanged.emit()
|
||||
|
||||
|
||||
def loadJsonFromReply(reply):
|
||||
try:
|
||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.logException("w", "Unable to decode JSON from reply.")
|
||||
return
|
||||
return result
|
||||
|
||||
|
||||
def checkValidGetReply(reply):
|
||||
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(list, key):
|
||||
for item in list:
|
||||
if item.key == key:
|
||||
return item
|
|
@ -0,0 +1,21 @@
|
|||
# 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.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class ClusterUM3PrinterOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
self.can_pre_heat_bed = False
|
||||
self.can_control_manually = False
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
data = "{\"action\": \"%s\"}" % state
|
||||
self._output_device.put("print_jobs/%s/action" % job.key, data, onFinished=None)
|
||||
|
|
@ -12,7 +12,10 @@ from cura.MachineAction import MachineAction
|
|||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class DiscoverUM3Action(MachineAction):
|
||||
discoveredDevicesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
|
||||
self._qml_url = "DiscoverUM3Action.qml"
|
||||
|
@ -25,24 +28,24 @@ class DiscoverUM3Action(MachineAction):
|
|||
|
||||
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
|
||||
|
||||
self._last_zeroconf_event_time = time.time()
|
||||
self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
|
||||
self._last_zero_conf_event_time = time.time()
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
# Time to wait after a zero-conf service change before allowing a zeroconf reset
|
||||
self._zero_conf_change_grace_period = 0.25
|
||||
|
||||
@pyqtSlot()
|
||||
def startDiscovery(self):
|
||||
if not self._network_plugin:
|
||||
Logger.log("d", "Starting printer discovery.")
|
||||
Logger.log("d", "Starting device discovery.")
|
||||
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
|
||||
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
|
||||
self.printersChanged.emit()
|
||||
self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
## Re-filters the list of printers.
|
||||
## Re-filters the list of devices.
|
||||
@pyqtSlot()
|
||||
def reset(self):
|
||||
Logger.log("d", "Reset the list of found printers.")
|
||||
self.printersChanged.emit()
|
||||
Logger.log("d", "Reset the list of found devices.")
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def restartDiscovery(self):
|
||||
|
@ -51,43 +54,44 @@ class DiscoverUM3Action(MachineAction):
|
|||
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
|
||||
# garbage collected the data.
|
||||
# Whatever the case, waiting a bit ensures that it doesn't crash.
|
||||
if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period:
|
||||
if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period:
|
||||
if not self._network_plugin:
|
||||
self.startDiscovery()
|
||||
else:
|
||||
self._network_plugin.startDiscovery()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def removeManualPrinter(self, key, address):
|
||||
def removeManualDevice(self, key, address):
|
||||
if not self._network_plugin:
|
||||
return
|
||||
|
||||
self._network_plugin.removeManualPrinter(key, address)
|
||||
self._network_plugin.removeManualDevice(key, address)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def setManualPrinter(self, key, address):
|
||||
def setManualDevice(self, key, address):
|
||||
if key != "":
|
||||
# This manual printer replaces a current manual printer
|
||||
self._network_plugin.removeManualPrinter(key)
|
||||
self._network_plugin.removeManualDevice(key)
|
||||
|
||||
if address != "":
|
||||
self._network_plugin.addManualPrinter(address)
|
||||
self._network_plugin.addManualDevice(address)
|
||||
|
||||
def _onPrinterDiscoveryChanged(self, *args):
|
||||
self._last_zeroconf_event_time = time.time()
|
||||
self.printersChanged.emit()
|
||||
def _onDeviceDiscoveryChanged(self, *args):
|
||||
self._last_zero_conf_event_time = time.time()
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
|
||||
def foundDevices(self):
|
||||
if self._network_plugin:
|
||||
# TODO: Check if this needs to stay.
|
||||
if Application.getInstance().getGlobalContainerStack():
|
||||
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
|
||||
else:
|
||||
global_printer_type = "unknown"
|
||||
|
||||
printers = list(self._network_plugin.getPrinters().values())
|
||||
printers = list(self._network_plugin.getDiscoveredDevices().values())
|
||||
# TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet.
|
||||
printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
|
||||
#printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
|
||||
printers.sort(key = lambda k: k.name)
|
||||
return printers
|
||||
else:
|
||||
|
|
|
@ -10,7 +10,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: base
|
||||
anchors.fill: parent;
|
||||
property var selectedPrinter: null
|
||||
property var selectedDevice: null
|
||||
property bool completeProperties: true
|
||||
|
||||
Connections
|
||||
|
@ -29,9 +29,9 @@ Cura.MachineAction
|
|||
|
||||
function connectToPrinter()
|
||||
{
|
||||
if(base.selectedPrinter && base.completeProperties)
|
||||
if(base.selectedDevice && base.completeProperties)
|
||||
{
|
||||
var printerKey = base.selectedPrinter.getKey()
|
||||
var printerKey = base.selectedDevice.key
|
||||
if(manager.getStoredKey() != printerKey)
|
||||
{
|
||||
manager.setKey(printerKey);
|
||||
|
@ -83,10 +83,10 @@ Cura.MachineAction
|
|||
{
|
||||
id: editButton
|
||||
text: catalog.i18nc("@action:button", "Edit")
|
||||
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
|
||||
enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
|
||||
onClicked:
|
||||
{
|
||||
manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress);
|
||||
manualPrinterDialog.showDialog(base.selectedDevice.key, base.selectedDevice.ipAddress);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,8 +94,8 @@ Cura.MachineAction
|
|||
{
|
||||
id: removeButton
|
||||
text: catalog.i18nc("@action:button", "Remove")
|
||||
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
|
||||
onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress)
|
||||
enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
|
||||
onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress)
|
||||
}
|
||||
|
||||
Button
|
||||
|
@ -139,7 +139,7 @@ Cura.MachineAction
|
|||
{
|
||||
var selectedKey = manager.getStoredKey();
|
||||
for(var i = 0; i < model.length; i++) {
|
||||
if(model[i].getKey() == selectedKey)
|
||||
if(model[i].key == selectedKey)
|
||||
{
|
||||
currentIndex = i;
|
||||
return
|
||||
|
@ -151,9 +151,9 @@ Cura.MachineAction
|
|||
currentIndex: -1
|
||||
onCurrentIndexChanged:
|
||||
{
|
||||
base.selectedPrinter = listview.model[currentIndex];
|
||||
base.selectedDevice = listview.model[currentIndex];
|
||||
// Only allow connecting if the printer has responded to API query since the last refresh
|
||||
base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true";
|
||||
base.completeProperties = base.selectedDevice != null && base.selectedDevice.getProperty("incomplete") != "true";
|
||||
}
|
||||
Component.onCompleted: manager.startDiscovery()
|
||||
delegate: Rectangle
|
||||
|
@ -199,13 +199,13 @@ Cura.MachineAction
|
|||
Column
|
||||
{
|
||||
width: Math.floor(parent.width * 0.5)
|
||||
visible: base.selectedPrinter ? true : false
|
||||
visible: base.selectedDevice ? true : false
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
Label
|
||||
{
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedPrinter ? base.selectedPrinter.name : ""
|
||||
text: base.selectedDevice ? base.selectedDevice.name : ""
|
||||
font: UM.Theme.getFont("large")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
@ -226,17 +226,17 @@ Cura.MachineAction
|
|||
wrapMode: Text.WordWrap
|
||||
text:
|
||||
{
|
||||
if(base.selectedPrinter)
|
||||
if(base.selectedDevice)
|
||||
{
|
||||
if(base.selectedPrinter.printerType == "ultimaker3")
|
||||
if(base.selectedDevice.printerType == "ultimaker3")
|
||||
{
|
||||
return catalog.i18nc("@label Printer name", "Ultimaker 3")
|
||||
} else if(base.selectedPrinter.printerType == "ultimaker3_extended")
|
||||
return catalog.i18nc("@label", "Ultimaker 3")
|
||||
} else if(base.selectedDevice.printerType == "ultimaker3_extended")
|
||||
{
|
||||
return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended")
|
||||
return catalog.i18nc("@label", "Ultimaker 3 Extended")
|
||||
} else
|
||||
{
|
||||
return catalog.i18nc("@label Printer name", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
|
||||
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -255,7 +255,7 @@ Cura.MachineAction
|
|||
{
|
||||
width: Math.floor(parent.width * 0.5)
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : ""
|
||||
text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
|
||||
}
|
||||
Label
|
||||
{
|
||||
|
@ -267,7 +267,7 @@ Cura.MachineAction
|
|||
{
|
||||
width: Math.floor(parent.width * 0.5)
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
|
||||
text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,17 +277,17 @@ Cura.MachineAction
|
|||
wrapMode: Text.WordWrap
|
||||
text:{
|
||||
// The property cluster size does not exist for older UM3 devices.
|
||||
if(!base.selectedPrinter || base.selectedPrinter.clusterSize == null || base.selectedPrinter.clusterSize == 1)
|
||||
if(!base.selectedDevice || base.selectedDevice.clusterSize == null || base.selectedDevice.clusterSize == 1)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else if (base.selectedPrinter.clusterSize === 0)
|
||||
else if (base.selectedDevice.clusterSize === 0)
|
||||
{
|
||||
return catalog.i18nc("@label", "This printer is not set up to host a group of Ultimaker 3 printers.");
|
||||
}
|
||||
else
|
||||
{
|
||||
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedPrinter.clusterSize));
|
||||
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedDevice.clusterSize));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -296,14 +296,14 @@ Cura.MachineAction
|
|||
{
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: base.selectedPrinter != null && !base.completeProperties
|
||||
visible: base.selectedDevice != null && !base.completeProperties
|
||||
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Connect")
|
||||
enabled: (base.selectedPrinter && base.completeProperties) ? true : false
|
||||
enabled: (base.selectedDevice && base.completeProperties) ? true : false
|
||||
onClicked: connectToPrinter()
|
||||
}
|
||||
}
|
||||
|
@ -337,7 +337,7 @@ Cura.MachineAction
|
|||
|
||||
onAccepted:
|
||||
{
|
||||
manager.setManualPrinter(printerKey, addressText)
|
||||
manager.setManualDevice(printerKey, addressText)
|
||||
}
|
||||
|
||||
Column {
|
||||
|
|
626
plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
Normal file
626
plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
Normal file
|
@ -0,0 +1,626 @@
|
|||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from cura.PrinterOutput.NetworkCamera import NetworkCamera
|
||||
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Application import Application
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
from PyQt5.QtCore import QTimer, QCoreApplication
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
|
||||
|
||||
from time import time
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
|
||||
# Everything after that firmware uses the ClusterUM3Output.
|
||||
# The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
|
||||
#
|
||||
# Authentication is done in a number of steps;
|
||||
# 1. Request an id / key pair by sending the application & user name. (state = authRequested)
|
||||
# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
|
||||
# 3. OutputDevice will poll if the button was pressed.
|
||||
# 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
|
||||
# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
|
||||
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
|
||||
def __init__(self, device_id, address: str, properties, parent = None):
|
||||
super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
|
||||
self._api_prefix = "/api/v1/"
|
||||
self._number_of_extruders = 2
|
||||
|
||||
self._authentication_id = None
|
||||
self._authentication_key = None
|
||||
|
||||
self._authentication_counter = 0
|
||||
self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
|
||||
|
||||
self._authentication_timer = QTimer()
|
||||
self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
|
||||
self._authentication_timer.setSingleShot(False)
|
||||
|
||||
self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
|
||||
|
||||
# The messages are created when connect is called the first time.
|
||||
# This ensures that the messages are only created for devices that actually want to connect.
|
||||
self._authentication_requested_message = None
|
||||
self._authentication_failed_message = None
|
||||
self._authentication_succeeded_message = None
|
||||
self._not_authenticated_message = None
|
||||
|
||||
self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)
|
||||
|
||||
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.setIconName("print")
|
||||
|
||||
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
|
||||
|
||||
self._output_controller = LegacyUM3PrinterOutputController(self)
|
||||
|
||||
def _onAuthenticationStateChanged(self):
|
||||
# We only accept commands if we are authenticated.
|
||||
if self._authentication_state == AuthState.Authenticated:
|
||||
self._setAcceptsCommands(True)
|
||||
else:
|
||||
self._setAcceptsCommands(False)
|
||||
|
||||
def _setupMessages(self):
|
||||
self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
|
||||
"Access to the printer requested. Please approve the request on the printer"),
|
||||
lifetime=0, dismissable=False, progress=0,
|
||||
title=i18n_catalog.i18nc("@info:title",
|
||||
"Authentication status"))
|
||||
|
||||
self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""),
|
||||
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
||||
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
|
||||
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
|
||||
self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
|
||||
self._authentication_succeeded_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
|
||||
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
||||
|
||||
self._not_authenticated_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
|
||||
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
||||
self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
|
||||
None, i18n_catalog.i18nc("@info:tooltip",
|
||||
"Send access request to the printer"))
|
||||
self._not_authenticated_message.actionTriggered.connect(self._messageCallback)
|
||||
|
||||
def _messageCallback(self, message_id=None, action_id="Retry"):
|
||||
if action_id == "Request" or action_id == "Retry":
|
||||
if self._authentication_failed_message:
|
||||
self._authentication_failed_message.hide()
|
||||
if self._not_authenticated_message:
|
||||
self._not_authenticated_message.hide()
|
||||
|
||||
self._requestAuthentication()
|
||||
|
||||
def connect(self):
|
||||
super().connect()
|
||||
self._setupMessages()
|
||||
global_container = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container:
|
||||
self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
|
||||
self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
if self._authentication_requested_message:
|
||||
self._authentication_requested_message.hide()
|
||||
if self._authentication_failed_message:
|
||||
self._authentication_failed_message.hide()
|
||||
if self._authentication_succeeded_message:
|
||||
self._authentication_succeeded_message.hide()
|
||||
self._sending_gcode = False
|
||||
self._compressing_gcode = False
|
||||
self._authentication_timer.stop()
|
||||
|
||||
## Send all material profiles to the printer.
|
||||
def _sendMaterialProfiles(self):
|
||||
Logger.log("i", "Sending material profiles to printer")
|
||||
|
||||
# TODO: Might want to move this to a job...
|
||||
for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
|
||||
try:
|
||||
xml_data = container.serialize()
|
||||
if xml_data == "" or xml_data is None:
|
||||
continue
|
||||
|
||||
names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
|
||||
if names:
|
||||
# There are other materials that share this GUID.
|
||||
if not container.isReadOnly():
|
||||
continue # If it's not readonly, it's created by user, so skip it.
|
||||
|
||||
file_name = "none.xml"
|
||||
|
||||
self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None)
|
||||
|
||||
except NotImplementedError:
|
||||
# If the material container is not the most "generic" one it can't be serialized an will raise a
|
||||
# NotImplementedError. We can simply ignore these.
|
||||
pass
|
||||
|
||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
|
||||
if not self.activePrinter:
|
||||
# No active printer. Unable to write
|
||||
return
|
||||
|
||||
if self.activePrinter.state not in ["idle", ""]:
|
||||
# Printer is not able to accept commands.
|
||||
return
|
||||
|
||||
if self._authentication_state != AuthState.Authenticated:
|
||||
# Not authenticated, so unable to send job.
|
||||
return
|
||||
|
||||
# Notify the UI that a switch to the print monitor should happen
|
||||
Application.getInstance().getController().setActiveStage("MonitorStage")
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", [])
|
||||
if not self._gcode:
|
||||
# Unable to find g-code. Nothing to send
|
||||
return
|
||||
|
||||
errors = self._checkForErrors()
|
||||
if errors:
|
||||
text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
|
||||
informative_text = i18n_catalog.i18nc("@label",
|
||||
"There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
|
||||
"Please resolve this issues before continuing.")
|
||||
detailed_text = ""
|
||||
for error in errors:
|
||||
detailed_text += error + "\n"
|
||||
|
||||
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
|
||||
text,
|
||||
informative_text,
|
||||
detailed_text,
|
||||
buttons=QMessageBox.Ok,
|
||||
icon=QMessageBox.Critical,
|
||||
callback = self._messageBoxCallback
|
||||
)
|
||||
return # Don't continue; Errors must block sending the job to the printer.
|
||||
|
||||
# There might be multiple things wrong with the configuration. Check these before starting.
|
||||
warnings = self._checkForWarnings()
|
||||
|
||||
if warnings:
|
||||
text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
|
||||
informative_text = i18n_catalog.i18nc("@label",
|
||||
"There is a mismatch between the configuration or calibration of the printer and Cura. "
|
||||
"For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
|
||||
detailed_text = ""
|
||||
for warning in warnings:
|
||||
detailed_text += warning + "\n"
|
||||
|
||||
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
|
||||
text,
|
||||
informative_text,
|
||||
detailed_text,
|
||||
buttons=QMessageBox.Yes + QMessageBox.No,
|
||||
icon=QMessageBox.Question,
|
||||
callback=self._messageBoxCallback
|
||||
)
|
||||
return
|
||||
|
||||
# No warnings or errors, so we're good to go.
|
||||
self._startPrint()
|
||||
|
||||
def _startPrint(self):
|
||||
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()
|
||||
return
|
||||
|
||||
self._sending_gcode = True
|
||||
|
||||
self._send_gcode_start = time()
|
||||
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
|
||||
i18n_catalog.i18nc("@info:title", "Sending Data"))
|
||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
|
||||
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
|
||||
self._progress_message.show()
|
||||
|
||||
compressed_gcode = self._compressGCode()
|
||||
if compressed_gcode is None:
|
||||
# Abort was called.
|
||||
return
|
||||
|
||||
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
|
||||
self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
|
||||
onFinished=self._onPostPrintJobFinished)
|
||||
|
||||
return
|
||||
|
||||
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
|
||||
if action_id == "Abort":
|
||||
Logger.log("d", "User aborted sending print to remote.")
|
||||
self._progress_message.hide()
|
||||
self._compressing_gcode = False
|
||||
self._sending_gcode = False
|
||||
Application.getInstance().getController().setActiveStage("PrepareStage")
|
||||
|
||||
def _onPostPrintJobFinished(self, reply):
|
||||
self._progress_message.hide()
|
||||
self._sending_gcode = False
|
||||
|
||||
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
|
||||
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 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)
|
||||
else:
|
||||
self._progress_message.setProgress(0)
|
||||
|
||||
self._progress_message.hide()
|
||||
|
||||
def _messageBoxCallback(self, button):
|
||||
def delayedCallback():
|
||||
if button == QMessageBox.Yes:
|
||||
self._startPrint()
|
||||
else:
|
||||
Application.getInstance().getController().setActiveStage("PrepareStage")
|
||||
# For some unknown reason Cura on OSX will hang if we do the call back code
|
||||
# immediately without first returning and leaving QML's event system.
|
||||
|
||||
QTimer.singleShot(100, delayedCallback)
|
||||
|
||||
def _checkForErrors(self):
|
||||
errors = []
|
||||
print_information = Application.getInstance().getPrintInformation()
|
||||
if not print_information.materialLengths:
|
||||
Logger.log("w", "There is no material length information. Unable to check for errors.")
|
||||
return errors
|
||||
|
||||
for index, extruder in enumerate(self.activePrinter.extruders):
|
||||
# Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
|
||||
if extruder.hotendID == "":
|
||||
# No Printcore loaded.
|
||||
errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))
|
||||
|
||||
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
|
||||
# The extruder is by this print.
|
||||
if extruder.activeMaterial is None:
|
||||
# No active material
|
||||
errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
|
||||
return errors
|
||||
|
||||
def _checkForWarnings(self):
|
||||
warnings = []
|
||||
print_information = Application.getInstance().getPrintInformation()
|
||||
|
||||
if not print_information.materialLengths:
|
||||
Logger.log("w", "There is no material length information. Unable to check for warnings.")
|
||||
return warnings
|
||||
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
|
||||
for index, extruder in enumerate(self.activePrinter.extruders):
|
||||
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
|
||||
# The extruder is by this print.
|
||||
|
||||
# TODO: material length check
|
||||
|
||||
# Check if the right Printcore is active.
|
||||
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
|
||||
if variant:
|
||||
if variant.getName() != extruder.hotendID:
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
|
||||
else:
|
||||
Logger.log("w", "Unable to find variant.")
|
||||
|
||||
# Check if the right material is loaded.
|
||||
local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
|
||||
if local_material:
|
||||
if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
|
||||
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
|
||||
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
|
||||
else:
|
||||
Logger.log("w", "Unable to find material.")
|
||||
|
||||
return warnings
|
||||
|
||||
def _update(self):
|
||||
if not super()._update():
|
||||
return
|
||||
if self._authentication_state == AuthState.NotAuthenticated:
|
||||
if self._authentication_id is None and self._authentication_key is None:
|
||||
# This machine doesn't have any authentication, so request it.
|
||||
self._requestAuthentication()
|
||||
elif self._authentication_id is not None and self._authentication_key is not None:
|
||||
# We have authentication info, but we haven't checked it out yet. Do so now.
|
||||
self._verifyAuthentication()
|
||||
elif self._authentication_state == AuthState.AuthenticationReceived:
|
||||
# We have an authentication, but it's not confirmed yet.
|
||||
self._checkAuthentication()
|
||||
|
||||
# We don't need authentication for requesting info, so we can go right ahead with requesting this.
|
||||
self.get("printer", onFinished=self._onGetPrinterDataFinished)
|
||||
self.get("print_job", onFinished=self._onGetPrintJobFinished)
|
||||
|
||||
def _resetAuthenticationRequestedMessage(self):
|
||||
if self._authentication_requested_message:
|
||||
self._authentication_requested_message.hide()
|
||||
self._authentication_timer.stop()
|
||||
self._authentication_counter = 0
|
||||
|
||||
def _onAuthenticationTimer(self):
|
||||
self._authentication_counter += 1
|
||||
self._authentication_requested_message.setProgress(
|
||||
self._authentication_counter / self._max_authentication_counter * 100)
|
||||
if self._authentication_counter > self._max_authentication_counter:
|
||||
self._authentication_timer.stop()
|
||||
Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
|
||||
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
||||
self._resetAuthenticationRequestedMessage()
|
||||
self._authentication_failed_message.show()
|
||||
|
||||
def _verifyAuthentication(self):
|
||||
Logger.log("d", "Attempting to verify authentication")
|
||||
# This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
|
||||
self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted)
|
||||
|
||||
def _onVerifyAuthenticationCompleted(self, reply):
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status_code == 401:
|
||||
# Something went wrong; We somehow tried to verify authentication without having one.
|
||||
Logger.log("d", "Attempted to verify auth without having one.")
|
||||
self._authentication_id = None
|
||||
self._authentication_key = None
|
||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||
elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
|
||||
# If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
|
||||
Logger.log("d",
|
||||
"While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
|
||||
self._authentication_state)
|
||||
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
||||
self._authentication_failed_message.show()
|
||||
elif status_code == 200:
|
||||
self.setAuthenticationState(AuthState.Authenticated)
|
||||
# Now we know for sure that we are authenticated, send the material profiles to the machine.
|
||||
self._sendMaterialProfiles()
|
||||
|
||||
def _checkAuthentication(self):
|
||||
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
|
||||
self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished)
|
||||
|
||||
def _onCheckAuthenticationFinished(self, reply):
|
||||
if str(self._authentication_id) not in reply.url().toString():
|
||||
Logger.log("w", "Got an old id response.")
|
||||
# Got response for old authentication ID.
|
||||
return
|
||||
try:
|
||||
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
|
||||
return
|
||||
|
||||
if data.get("message", "") == "authorized":
|
||||
Logger.log("i", "Authentication was approved")
|
||||
self.setAuthenticationState(AuthState.Authenticated)
|
||||
self._saveAuthentication()
|
||||
|
||||
# Double check that everything went well.
|
||||
self._verifyAuthentication()
|
||||
|
||||
# Notify the user.
|
||||
self._resetAuthenticationRequestedMessage()
|
||||
self._authentication_succeeded_message.show()
|
||||
elif data.get("message", "") == "unauthorized":
|
||||
Logger.log("i", "Authentication was denied.")
|
||||
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
||||
self._authentication_failed_message.show()
|
||||
|
||||
def _saveAuthentication(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if "network_authentication_key" in global_container_stack.getMetaData():
|
||||
global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
|
||||
else:
|
||||
global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key)
|
||||
|
||||
if "network_authentication_id" in global_container_stack.getMetaData():
|
||||
global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
|
||||
else:
|
||||
global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
|
||||
|
||||
# Force save so we are sure the data is not lost.
|
||||
Application.getInstance().saveStack(global_container_stack)
|
||||
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
|
||||
self._getSafeAuthKey())
|
||||
else:
|
||||
Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
|
||||
self._getSafeAuthKey())
|
||||
|
||||
def _onRequestAuthenticationFinished(self, reply):
|
||||
try:
|
||||
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
|
||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||
return
|
||||
|
||||
self.setAuthenticationState(AuthState.AuthenticationReceived)
|
||||
self._authentication_id = data["id"]
|
||||
self._authentication_key = data["key"]
|
||||
Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
|
||||
self._authentication_id, self._getSafeAuthKey())
|
||||
|
||||
def _requestAuthentication(self):
|
||||
self._authentication_requested_message.show()
|
||||
self._authentication_timer.start()
|
||||
|
||||
# Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
|
||||
# give issues.
|
||||
self._authentication_key = None
|
||||
self._authentication_id = None
|
||||
|
||||
self.post("auth/request",
|
||||
json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
|
||||
"user": self._getUserName()}).encode(),
|
||||
onFinished=self._onRequestAuthenticationFinished)
|
||||
|
||||
self.setAuthenticationState(AuthState.AuthenticationRequested)
|
||||
|
||||
def _onAuthenticationRequired(self, reply, authenticator):
|
||||
if self._authentication_id is not None and self._authentication_key is not None:
|
||||
Logger.log("d",
|
||||
"Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
|
||||
self._id, self._authentication_id, self._getSafeAuthKey())
|
||||
authenticator.setUser(self._authentication_id)
|
||||
authenticator.setPassword(self._authentication_key)
|
||||
else:
|
||||
Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)
|
||||
|
||||
def _onGetPrintJobFinished(self, reply):
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
|
||||
if not self._printers:
|
||||
return # Ignore the data for now, we don't have info about a printer yet.
|
||||
printer = self._printers[0]
|
||||
|
||||
if status_code == 200:
|
||||
try:
|
||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
|
||||
return
|
||||
if printer.activePrintJob is None:
|
||||
print_job = PrintJobOutputModel(output_controller=self._output_controller)
|
||||
printer.updateActivePrintJob(print_job)
|
||||
else:
|
||||
print_job = printer.activePrintJob
|
||||
print_job.updateState(result["state"])
|
||||
print_job.updateTimeElapsed(result["time_elapsed"])
|
||||
print_job.updateTimeTotal(result["time_total"])
|
||||
print_job.updateName(result["name"])
|
||||
elif status_code == 404:
|
||||
# No job found, so delete the active print job (if any!)
|
||||
printer.updateActivePrintJob(None)
|
||||
else:
|
||||
Logger.log("w",
|
||||
"Got status code {status_code} while trying to get printer data".format(status_code=status_code))
|
||||
|
||||
def materialHotendChangedMessage(self, callback):
|
||||
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
|
||||
i18n_catalog.i18nc("@label",
|
||||
"Would you like to use your current printer configuration in Cura?"),
|
||||
i18n_catalog.i18nc("@label",
|
||||
"The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
|
||||
buttons=QMessageBox.Yes + QMessageBox.No,
|
||||
icon=QMessageBox.Question,
|
||||
callback=callback
|
||||
)
|
||||
|
||||
def _onGetPrinterDataFinished(self, reply):
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status_code == 200:
|
||||
try:
|
||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
|
||||
return
|
||||
|
||||
if not self._printers:
|
||||
# Quickest way to get the firmware version is to grab it from the zeroconf.
|
||||
firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
|
||||
self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
|
||||
self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream"))
|
||||
for extruder in self._printers[0].extruders:
|
||||
extruder.activeMaterialChanged.connect(self.materialIdChanged)
|
||||
extruder.hotendIDChanged.connect(self.hotendIdChanged)
|
||||
self.printersChanged.emit()
|
||||
|
||||
# LegacyUM3 always has a single printer.
|
||||
printer = self._printers[0]
|
||||
printer.updateBedTemperature(result["bed"]["temperature"]["current"])
|
||||
printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
|
||||
printer.updateState(result["status"])
|
||||
|
||||
try:
|
||||
# If we're still handling the request, we should ignore remote for a bit.
|
||||
if not printer.getController().isPreheatRequestInProgress():
|
||||
printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
|
||||
except KeyError:
|
||||
# Older firmwares don't support preheating, so we need to fake it.
|
||||
pass
|
||||
|
||||
head_position = result["heads"][0]["position"]
|
||||
printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])
|
||||
|
||||
for index in range(0, self._number_of_extruders):
|
||||
temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
|
||||
extruder = printer.extruders[index]
|
||||
extruder.updateTargetHotendTemperature(temperatures["target"])
|
||||
extruder.updateHotendTemperature(temperatures["current"])
|
||||
|
||||
material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]
|
||||
|
||||
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
|
||||
# Find matching material (as we need to set brand, type & color)
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
|
||||
GUID=material_guid)
|
||||
if containers:
|
||||
color = containers[0].getMetaDataEntry("color_code")
|
||||
brand = containers[0].getMetaDataEntry("brand")
|
||||
material_type = containers[0].getMetaDataEntry("material")
|
||||
name = containers[0].getName()
|
||||
else:
|
||||
# Unknown material.
|
||||
color = "#00000000"
|
||||
brand = "Unknown"
|
||||
material_type = "Unknown"
|
||||
name = "Unknown"
|
||||
material = MaterialOutputModel(guid=material_guid, type=material_type,
|
||||
brand=brand, color=color, name = name)
|
||||
extruder.updateActiveMaterial(material)
|
||||
|
||||
try:
|
||||
hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
|
||||
except KeyError:
|
||||
hotend_id = ""
|
||||
printer.extruders[index].updateHotendID(hotend_id)
|
||||
|
||||
else:
|
||||
Logger.log("w",
|
||||
"Got status code {status_code} while trying to get printer data".format(status_code = status_code))
|
||||
|
||||
## Convenience function to "blur" out all but the last 5 characters of the auth key.
|
||||
# This can be used to debug print the key, without it compromising the security.
|
||||
def _getSafeAuthKey(self):
|
||||
if self._authentication_key is not None:
|
||||
result = self._authentication_key[-5:]
|
||||
result = "********" + result
|
||||
return result
|
||||
|
||||
return self._authentication_key
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
from UM.Version import Version
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class LegacyUM3PrinterOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
self._preheat_bed_timer = QTimer()
|
||||
self._preheat_bed_timer.setSingleShot(True)
|
||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
||||
self._preheat_printer = None
|
||||
|
||||
self.can_control_manually = False
|
||||
|
||||
# Are we still waiting for a response about preheat?
|
||||
# We need this so we can already update buttons, so it feels more snappy.
|
||||
self._preheat_request_in_progress = False
|
||||
|
||||
def isPreheatRequestInProgress(self):
|
||||
return self._preheat_request_in_progress
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
data = "{\"target\": \"%s\"}" % state
|
||||
self._output_device.put("print_job/state", data, onFinished=None)
|
||||
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||
data = str(temperature)
|
||||
self._output_device.put("printer/bed/temperature/target", data, onFinished=self._onPutBedTemperatureCompleted)
|
||||
|
||||
def _onPutBedTemperatureCompleted(self, reply):
|
||||
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
|
||||
# If it was handling a preheat, it isn't anymore.
|
||||
self._preheat_request_in_progress = False
|
||||
|
||||
def _onPutPreheatBedCompleted(self, reply):
|
||||
self._preheat_request_in_progress = False
|
||||
|
||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||
head_pos = printer._head_position
|
||||
new_x = head_pos.x + x
|
||||
new_y = head_pos.y + y
|
||||
new_z = head_pos.z + z
|
||||
data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z)
|
||||
self._output_device.put("printer/heads/0/position", data, onFinished=None)
|
||||
|
||||
def homeBed(self, printer):
|
||||
self._output_device.put("printer/heads/0/position/z", "0", onFinished=None)
|
||||
|
||||
def _onPreheatBedTimerFinished(self):
|
||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||
self._preheat_printer.updateIsPreheating(False)
|
||||
self._preheat_request_in_progress = True
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||
self.preheatBed(printer, temperature=0, duration=0)
|
||||
self._preheat_bed_timer.stop()
|
||||
printer.updateIsPreheating(False)
|
||||
|
||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||
try:
|
||||
temperature = round(temperature) # The API doesn't allow floating point.
|
||||
duration = round(duration)
|
||||
except ValueError:
|
||||
return # Got invalid values, can't pre-heat.
|
||||
|
||||
if duration > 0:
|
||||
data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration)
|
||||
else:
|
||||
data = """{"temperature": "%i"}""" % temperature
|
||||
|
||||
# Real bed pre-heating support is implemented from 3.5.92 and up.
|
||||
|
||||
if Version(printer.firmwareVersion) < Version("3.5.92"):
|
||||
# No firmware-side duration support then, so just set target bed temp and set a timer.
|
||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||
self._preheat_bed_timer.start()
|
||||
self._preheat_printer = printer
|
||||
printer.updateIsPreheating(True)
|
||||
return
|
||||
|
||||
self._output_device.put("printer/bed/pre_heat", data, onFinished = self._onPutPreheatBedCompleted)
|
||||
printer.updateIsPreheating(True)
|
||||
self._preheat_request_in_progress = True
|
||||
|
||||
|
|
@ -6,40 +6,49 @@ import Cura 1.0 as Cura
|
|||
|
||||
Component
|
||||
{
|
||||
Image
|
||||
Item
|
||||
{
|
||||
id: cameraImage
|
||||
property bool proportionalHeight:
|
||||
width: maximumWidth
|
||||
height: maximumHeight
|
||||
Image
|
||||
{
|
||||
if(sourceSize.height == 0 || maximumHeight == 0)
|
||||
id: cameraImage
|
||||
width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
|
||||
height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
z: 1
|
||||
Component.onCompleted:
|
||||
{
|
||||
return true;
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.start()
|
||||
}
|
||||
}
|
||||
return (sourceSize.width / sourceSize.height) > (maximumWidth / maximumHeight);
|
||||
}
|
||||
property real _width: Math.floor(Math.min(maximumWidth, sourceSize.width))
|
||||
property real _height: Math.floor(Math.min(maximumHeight, sourceSize.height))
|
||||
width: proportionalHeight ? _width : Math.floor(sourceSize.width * _height / sourceSize.height)
|
||||
height: !proportionalHeight ? _height : Math.floor(sourceSize.height * _width / sourceSize.width)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
if(visible)
|
||||
onVisibleChanged:
|
||||
{
|
||||
OutputDevice.startCamera()
|
||||
} else
|
||||
{
|
||||
OutputDevice.stopCamera()
|
||||
if(visible)
|
||||
{
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.start()
|
||||
}
|
||||
} else
|
||||
{
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
source:
|
||||
{
|
||||
if(OutputDevice.cameraImage)
|
||||
source:
|
||||
{
|
||||
return OutputDevice.cameraImage;
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
|
||||
{
|
||||
return OutputDevice.activePrinter.camera.latestImage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,735 +0,0 @@
|
|||
import datetime
|
||||
import getpass
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import time
|
||||
|
||||
from enum import Enum
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.OutputDevice import OutputDeviceError
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Qt.Duration import Duration, DurationFormat
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
from . import NetworkPrinterOutputDevice
|
||||
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class OutputStage(Enum):
|
||||
ready = 0
|
||||
uploading = 2
|
||||
|
||||
|
||||
class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice):
|
||||
printJobsChanged = pyqtSignal()
|
||||
printersChanged = pyqtSignal()
|
||||
selectedPrinterChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, key, address, properties, api_prefix):
|
||||
super().__init__(key, address, properties, api_prefix)
|
||||
# Store the address of the master.
|
||||
self._master_address = address
|
||||
name_property = properties.get(b"name", b"")
|
||||
if name_property:
|
||||
name = name_property.decode("utf-8")
|
||||
else:
|
||||
name = key
|
||||
|
||||
self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated
|
||||
|
||||
self.setName(name)
|
||||
description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")
|
||||
self.setShortDescription(description)
|
||||
self.setDescription(description)
|
||||
|
||||
self._stage = OutputStage.ready
|
||||
host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "")
|
||||
if host_override:
|
||||
Logger.log(
|
||||
"w",
|
||||
"Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host",
|
||||
host_override)
|
||||
self._host = "http://" + host_override
|
||||
else:
|
||||
self._host = "http://" + address
|
||||
|
||||
# is the same as in NetworkPrinterOutputDevicePlugin
|
||||
self._cluster_api_version = "1"
|
||||
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
|
||||
self._api_base_uri = self._host + self._cluster_api_prefix
|
||||
|
||||
self._file_name = None
|
||||
self._progress_message = None
|
||||
self._request = None
|
||||
self._reply = None
|
||||
|
||||
# The main reason to keep the 'multipart' form data on the object
|
||||
# is to prevent the Python GC from claiming it too early.
|
||||
self._multipart = None
|
||||
|
||||
self._print_view = None
|
||||
self._request_job = []
|
||||
|
||||
self._job_list = []
|
||||
|
||||
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
|
||||
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
|
||||
|
||||
self._print_jobs = []
|
||||
self._print_job_by_printer_uuid = {}
|
||||
self._print_job_by_uuid = {} # Print jobs by their own uuid
|
||||
self._printers = []
|
||||
self._printers_dict = {} # by unique_name
|
||||
|
||||
self._connected_printers_type_count = []
|
||||
self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"} # empty unique_name IS automatic selection
|
||||
self._selected_printer = self._automatic_printer
|
||||
|
||||
self._cluster_status_update_timer = QTimer()
|
||||
self._cluster_status_update_timer.setInterval(5000)
|
||||
self._cluster_status_update_timer.setSingleShot(False)
|
||||
self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus)
|
||||
|
||||
self._can_pause = True
|
||||
self._can_abort = True
|
||||
self._can_pre_heat_bed = False
|
||||
self._can_control_manually = False
|
||||
self._cluster_size = int(properties.get(b"cluster_size", 0))
|
||||
|
||||
self._cleanupRequest()
|
||||
|
||||
#These are texts that are to be translated for future features.
|
||||
temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.")
|
||||
temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3)
|
||||
temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished.
|
||||
temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed.
|
||||
|
||||
## No authentication, so requestAuthentication should do exactly nothing
|
||||
@pyqtSlot()
|
||||
def requestAuthentication(self, message_id = None, action_id = "Retry"):
|
||||
pass # Cura Connect doesn't do any authorization
|
||||
|
||||
def setAuthenticationState(self, auth_state):
|
||||
self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated
|
||||
|
||||
def _verifyAuthentication(self):
|
||||
pass
|
||||
|
||||
def _checkAuthentication(self):
|
||||
Logger.log("d", "_checkAuthentication Cura Connect - nothing to be done")
|
||||
|
||||
@pyqtProperty(QObject, notify=selectedPrinterChanged)
|
||||
def controlItem(self):
|
||||
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
|
||||
if not self._control_item:
|
||||
self._createControlViewFromQML()
|
||||
name = self._selected_printer.get("friendly_name")
|
||||
if name == self._automatic_printer.get("friendly_name") or name == "":
|
||||
return self._control_item
|
||||
# Let cura use the default.
|
||||
return None
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getTimeCompleted(self, time_remaining):
|
||||
current_time = time.time()
|
||||
datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
|
||||
return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute)
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getDateCompleted(self, time_remaining):
|
||||
current_time = time.time()
|
||||
datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
|
||||
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def clusterSize(self):
|
||||
return self._cluster_size
|
||||
|
||||
@pyqtProperty(str, notify=selectedPrinterChanged)
|
||||
def name(self):
|
||||
# Show the name of the selected printer.
|
||||
# This is not the nicest way to do this, but changes to the Cura UI are required otherwise.
|
||||
name = self._selected_printer.get("friendly_name")
|
||||
if name != self._automatic_printer.get("friendly_name"):
|
||||
return name
|
||||
# Return name of cluster master.
|
||||
return self._properties.get(b"name", b"").decode("utf-8")
|
||||
|
||||
def connect(self):
|
||||
super().connect()
|
||||
self._cluster_status_update_timer.start()
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
self._cluster_status_update_timer.stop()
|
||||
|
||||
def _setJobState(self, job_state):
|
||||
if not self._selected_printer:
|
||||
return
|
||||
|
||||
selected_printer_uuid = self._printers_dict[self._selected_printer["unique_name"]]["uuid"]
|
||||
if selected_printer_uuid not in self._print_job_by_printer_uuid:
|
||||
return
|
||||
|
||||
print_job_uuid = self._print_job_by_printer_uuid[selected_printer_uuid]["uuid"]
|
||||
|
||||
url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action")
|
||||
put_request = QNetworkRequest(url)
|
||||
put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
|
||||
data = '{"action": "' + job_state + '"}'
|
||||
self._manager.put(put_request, data.encode())
|
||||
|
||||
def _requestClusterStatus(self):
|
||||
# TODO: Handle timeout. We probably want to know if the cluster is still reachable or not.
|
||||
url = QUrl(self._api_base_uri + "printers/")
|
||||
printers_request = QNetworkRequest(url)
|
||||
self._addUserAgentHeader(printers_request)
|
||||
self._manager.get(printers_request)
|
||||
# See _finishedPrintersRequest()
|
||||
|
||||
if self._printers: # if printers is not empty
|
||||
url = QUrl(self._api_base_uri + "print_jobs/")
|
||||
print_jobs_request = QNetworkRequest(url)
|
||||
self._addUserAgentHeader(print_jobs_request)
|
||||
self._manager.get(print_jobs_request)
|
||||
# See _finishedPrintJobsRequest()
|
||||
|
||||
def _finishedPrintJobsRequest(self, reply):
|
||||
try:
|
||||
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
|
||||
return
|
||||
self.setPrintJobs(json_data)
|
||||
|
||||
def _finishedPrintersRequest(self, reply):
|
||||
try:
|
||||
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
|
||||
return
|
||||
self.setPrinters(json_data)
|
||||
|
||||
def materialHotendChangedMessage(self, callback):
|
||||
# When there is just one printer, the activate configuration option is enabled
|
||||
if (self._cluster_size == 1):
|
||||
super().materialHotendChangedMessage(callback = callback)
|
||||
|
||||
def _startCameraStream(self):
|
||||
## Request new image
|
||||
url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream")
|
||||
self._image_request = QNetworkRequest(url)
|
||||
self._addUserAgentHeader(self._image_request)
|
||||
self._image_reply = self._manager.get(self._image_request)
|
||||
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
||||
|
||||
def spawnPrintView(self):
|
||||
if self._print_view is None:
|
||||
path = os.path.join(self._plugin_path, "PrintWindow.qml")
|
||||
self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
|
||||
if self._print_view is not None:
|
||||
self._print_view.show()
|
||||
|
||||
## Store job info, show Print view for settings
|
||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
|
||||
self._selected_printer = self._automatic_printer # reset to default option
|
||||
self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs]
|
||||
|
||||
# the build plates to be sent
|
||||
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")
|
||||
self._job_list = list(gcode_dict.keys())
|
||||
Logger.log("d", "build plates to be sent to printer: %s", (self._job_list))
|
||||
|
||||
if self._stage != OutputStage.ready:
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status",
|
||||
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
|
||||
self._error_message.show()
|
||||
return
|
||||
|
||||
self._add_build_plate_number = len(self._job_list) > 1
|
||||
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
|
||||
if len(self._printers) > 1:
|
||||
self.spawnPrintView() # Ask user how to print it.
|
||||
elif len(self._printers) == 1:
|
||||
# If there is only one printer, don't bother asking.
|
||||
self.selectAutomaticPrinter()
|
||||
self.sendPrintJob()
|
||||
|
||||
else:
|
||||
# Cluster has no printers, warn the user of this.
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status",
|
||||
"Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers."))
|
||||
self._error_message.show()
|
||||
|
||||
## Actually send the print job, called from the dialog
|
||||
# :param: require_printer_name: name of printer, or ""
|
||||
@pyqtSlot()
|
||||
def sendPrintJob(self):
|
||||
nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job
|
||||
output_build_plate_number = self._job_list.pop(0)
|
||||
gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_dict")[output_build_plate_number]
|
||||
if not gcode_list: # Empty build plate
|
||||
Logger.log("d", "Skipping empty job (build plate number %d).", output_build_plate_number)
|
||||
return self.sendPrintJob()
|
||||
|
||||
self._send_gcode_start = time.time()
|
||||
Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number))
|
||||
|
||||
if self._stage != OutputStage.ready:
|
||||
Logger.log("d", "Unable to send print job as the state is %s", self._stage)
|
||||
raise OutputDeviceError.DeviceBusyError()
|
||||
self._stage = OutputStage.uploading
|
||||
|
||||
if self._add_build_plate_number:
|
||||
self._file_name = "%s_%d.gcode.gz" % (file_name, output_build_plate_number)
|
||||
else:
|
||||
self._file_name = "%s.gcode.gz" % (file_name)
|
||||
self._showProgressMessage()
|
||||
|
||||
require_printer_name = self._selected_printer["unique_name"]
|
||||
|
||||
new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode_list)
|
||||
if new_request is None or self._stage != OutputStage.uploading:
|
||||
return
|
||||
self._request = new_request
|
||||
self._reply = self._manager.post(self._request, self._multipart)
|
||||
self._reply.uploadProgress.connect(self._onUploadProgress)
|
||||
# See _finishedPrintJobPostRequest()
|
||||
|
||||
def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode_list):
|
||||
api_url = QUrl(self._api_base_uri + "print_jobs/")
|
||||
request = QNetworkRequest(api_url)
|
||||
# Create multipart request and add the g-code.
|
||||
self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||
|
||||
# Add gcode
|
||||
part = QHttpPart()
|
||||
part.setHeader(QNetworkRequest.ContentDispositionHeader,
|
||||
'form-data; name="file"; filename="%s"' % (self._file_name))
|
||||
|
||||
compressed_gcode = self._compressGcode(gcode_list)
|
||||
if compressed_gcode is None:
|
||||
return None # User aborted print, so stop trying.
|
||||
|
||||
part.setBody(compressed_gcode)
|
||||
self._multipart.append(part)
|
||||
|
||||
# require_printer_name "" means automatic
|
||||
if require_printer_name:
|
||||
self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name))
|
||||
user_name = self.__get_username()
|
||||
if user_name is None:
|
||||
user_name = "unknown"
|
||||
self._multipart.append(self.__createKeyValueHttpPart("owner", user_name))
|
||||
|
||||
self._addUserAgentHeader(request)
|
||||
return request
|
||||
|
||||
def _compressGcode(self, gcode_list):
|
||||
self._compressing_print = True
|
||||
batched_line = ""
|
||||
max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB
|
||||
|
||||
byte_array_file_data = b""
|
||||
|
||||
def _compressDataAndNotifyQt(data_to_append):
|
||||
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
|
||||
self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
# Pretend that this is a response, as zipping might take a bit of time.
|
||||
self._last_response_time = time.time()
|
||||
return compressed_data
|
||||
|
||||
if gcode_list is None:
|
||||
Logger.log("e", "Unable to find sliced gcode, returning empty.")
|
||||
return byte_array_file_data
|
||||
|
||||
for line in gcode_list:
|
||||
if not self._compressing_print:
|
||||
self._progress_message.hide()
|
||||
return None # Stop trying to zip, abort was called.
|
||||
batched_line += line
|
||||
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
|
||||
# Compressing line by line in this case is extremely slow, so we need to batch them.
|
||||
if len(batched_line) < max_chars_per_line:
|
||||
continue
|
||||
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
|
||||
batched_line = ""
|
||||
|
||||
# Also compress the leftovers.
|
||||
if batched_line:
|
||||
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
|
||||
|
||||
return byte_array_file_data
|
||||
|
||||
def __createKeyValueHttpPart(self, key, value):
|
||||
metadata_part = QHttpPart()
|
||||
metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain')
|
||||
metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key))
|
||||
metadata_part.setBody(bytearray(value, "utf8"))
|
||||
return metadata_part
|
||||
|
||||
def __get_username(self):
|
||||
try:
|
||||
return getpass.getuser()
|
||||
except:
|
||||
Logger.log("d", "Could not get the system user name, returning 'unknown' instead.")
|
||||
return None
|
||||
|
||||
def _finishedPrintJobPostRequest(self, reply):
|
||||
self._stage = OutputStage.ready
|
||||
if self._progress_message:
|
||||
self._progress_message.hide()
|
||||
self._progress_message = None
|
||||
self.writeFinished.emit(self)
|
||||
|
||||
if reply.error():
|
||||
self._showRequestFailedMessage(reply)
|
||||
self.writeError.emit(self)
|
||||
else:
|
||||
self._showRequestSucceededMessage()
|
||||
self.writeSuccess.emit(self)
|
||||
|
||||
self._cleanupRequest()
|
||||
|
||||
if self._job_list: # start sending next job
|
||||
self.sendPrintJob()
|
||||
|
||||
def _showRequestFailedMessage(self, reply):
|
||||
if reply is not None:
|
||||
Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
|
||||
cluster_name = self.getName(),
|
||||
error_string = str(reply.errorString()),
|
||||
error = str(reply.error())))
|
||||
error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
|
||||
message = Message(text=error_message_template.format(
|
||||
cluster_name = self.getName()))
|
||||
message.show()
|
||||
|
||||
def _showRequestSucceededMessage(self):
|
||||
confirmation_message_template = i18n_catalog.i18nc(
|
||||
"@info:status",
|
||||
"Sent {file_name} to group {cluster_name}."
|
||||
)
|
||||
file_name = os.path.basename(self._file_name).split(".")[0]
|
||||
message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
|
||||
message = Message(text=message_text)
|
||||
button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
|
||||
button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
|
||||
message.addAction("open_browser", button_text, "globe", button_tooltip)
|
||||
message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||
message.show()
|
||||
|
||||
def setPrintJobs(self, print_jobs):
|
||||
#TODO: hack, last seen messes up the check, so drop it.
|
||||
for job in print_jobs:
|
||||
del job["last_seen"]
|
||||
# Strip any extensions
|
||||
job["name"] = self._removeGcodeExtension(job["name"])
|
||||
|
||||
if self._print_jobs != print_jobs:
|
||||
old_print_jobs = self._print_jobs
|
||||
self._print_jobs = print_jobs
|
||||
|
||||
self._notifyFinishedPrintJobs(old_print_jobs, print_jobs)
|
||||
self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs)
|
||||
|
||||
# Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer
|
||||
# for some reason. ugh.
|
||||
self._print_job_by_printer_uuid = {}
|
||||
self._print_job_by_uuid = {}
|
||||
for print_job in print_jobs:
|
||||
if "printer_uuid" in print_job and print_job["printer_uuid"] is not None:
|
||||
self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job
|
||||
self._print_job_by_uuid[print_job["uuid"]] = print_job
|
||||
self.printJobsChanged.emit()
|
||||
|
||||
def _removeGcodeExtension(self, name):
|
||||
parts = name.split(".")
|
||||
if parts[-1].upper() == "GZ":
|
||||
parts = parts[:-1]
|
||||
if parts[-1].upper() == "GCODE":
|
||||
parts = parts[:-1]
|
||||
return ".".join(parts)
|
||||
|
||||
def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs):
|
||||
"""Notify the user when any of their print jobs have just completed.
|
||||
|
||||
Arguments:
|
||||
|
||||
old_print_jobs -- the previous list of print job status information as returned by the cluster REST API.
|
||||
new_print_jobs -- the current list of print job status information as returned by the cluster REST API.
|
||||
"""
|
||||
if old_print_jobs is None:
|
||||
return
|
||||
|
||||
username = self.__get_username()
|
||||
if username is None:
|
||||
return
|
||||
|
||||
our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs)
|
||||
our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"]
|
||||
|
||||
our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs)
|
||||
our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"]
|
||||
|
||||
old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs])
|
||||
|
||||
for print_job in our_new_finished_print_jobs:
|
||||
if print_job["uuid"] in old_not_finished_print_job_uuids:
|
||||
|
||||
printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
|
||||
if printer_name is None:
|
||||
printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
|
||||
|
||||
message_text = (i18n_catalog.i18nc("@info:status",
|
||||
"Printer '{printer_name}' has finished printing '{job_name}'.")
|
||||
.format(printer_name=printer_name, job_name=print_job["name"]))
|
||||
message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished"))
|
||||
Application.getInstance().showMessage(message)
|
||||
Application.getInstance().showToastMessage(
|
||||
i18n_catalog.i18nc("@info:status", "Print finished"),
|
||||
message_text)
|
||||
|
||||
def __filterOurPrintJobs(self, print_jobs):
|
||||
username = self.__get_username()
|
||||
return [print_job for print_job in print_jobs if print_job["owner"] == username]
|
||||
|
||||
def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs):
|
||||
if old_print_jobs is None:
|
||||
return
|
||||
|
||||
old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs))
|
||||
new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs))
|
||||
old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs])
|
||||
|
||||
for print_job in new_change_required_print_jobs:
|
||||
if print_job["uuid"] not in old_change_required_print_job_uuids:
|
||||
|
||||
printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"])
|
||||
if printer_name is None:
|
||||
# don't report on yet unknown printers
|
||||
continue
|
||||
|
||||
message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.")
|
||||
.format(printer_name=printer_name, job_name=print_job["name"]))
|
||||
message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required"))
|
||||
Application.getInstance().showMessage(message)
|
||||
Application.getInstance().showToastMessage(
|
||||
i18n_catalog.i18nc("@label:status", "Action required"),
|
||||
message_text)
|
||||
|
||||
def __filterConfigChangePrintJobs(self, print_jobs):
|
||||
return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs)
|
||||
|
||||
def __isConfigurationChangeRequiredPrintJob(self, print_job):
|
||||
if print_job["status"] == "queued":
|
||||
changes_required = print_job.get("configuration_changes_required", [])
|
||||
return len(changes_required) != 0
|
||||
return False
|
||||
|
||||
def __getPrinterNameFromUuid(self, printer_uuid):
|
||||
for printer in self._printers:
|
||||
if printer["uuid"] == printer_uuid:
|
||||
return printer["friendly_name"]
|
||||
return None
|
||||
|
||||
def setPrinters(self, printers):
|
||||
if self._printers != printers:
|
||||
self._connected_printers_type_count = []
|
||||
printers_count = {}
|
||||
self._printers = printers
|
||||
self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name
|
||||
|
||||
for printer in printers:
|
||||
variant = printer["machine_variant"]
|
||||
if variant in printers_count:
|
||||
printers_count[variant] += 1
|
||||
else:
|
||||
printers_count[variant] = 1
|
||||
for type in printers_count:
|
||||
self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]})
|
||||
self.printersChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printersChanged)
|
||||
def connectedPrintersTypeCount(self):
|
||||
return self._connected_printers_type_count
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printersChanged)
|
||||
def connectedPrinters(self):
|
||||
return self._printers
|
||||
|
||||
@pyqtProperty(int, notify=printJobsChanged)
|
||||
def numJobsPrinting(self):
|
||||
num_jobs_printing = 0
|
||||
for job in self._print_jobs:
|
||||
if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]:
|
||||
num_jobs_printing += 1
|
||||
return num_jobs_printing
|
||||
|
||||
@pyqtProperty(int, notify=printJobsChanged)
|
||||
def numJobsQueued(self):
|
||||
num_jobs_queued = 0
|
||||
for job in self._print_jobs:
|
||||
if job["status"] == "queued":
|
||||
num_jobs_queued += 1
|
||||
return num_jobs_queued
|
||||
|
||||
@pyqtProperty("QVariantMap", notify=printJobsChanged)
|
||||
def printJobsByUUID(self):
|
||||
return self._print_job_by_uuid
|
||||
|
||||
@pyqtProperty("QVariantMap", notify=printJobsChanged)
|
||||
def printJobsByPrinterUUID(self):
|
||||
return self._print_job_by_printer_uuid
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printJobsChanged)
|
||||
def printJobs(self):
|
||||
return self._print_jobs
|
||||
|
||||
@pyqtProperty("QVariantList", notify=printersChanged)
|
||||
def printers(self):
|
||||
return [self._automatic_printer, ] + self._printers
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def selectPrinter(self, unique_name, friendly_name):
|
||||
self.stopCamera()
|
||||
self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name}
|
||||
Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name)
|
||||
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
|
||||
if unique_name == "":
|
||||
self._address = self._master_address
|
||||
else:
|
||||
self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"]
|
||||
|
||||
self.selectedPrinterChanged.emit()
|
||||
|
||||
def _updateJobState(self, job_state):
|
||||
name = self._selected_printer.get("friendly_name")
|
||||
if name == "" or name == "Automatic":
|
||||
# TODO: This is now a bit hacked; If no printer is selected, don't show job state.
|
||||
if self._job_state != "":
|
||||
self._job_state = ""
|
||||
self.jobStateChanged.emit()
|
||||
else:
|
||||
if self._job_state != job_state:
|
||||
self._job_state = job_state
|
||||
self.jobStateChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def selectAutomaticPrinter(self):
|
||||
self.stopCamera()
|
||||
self._selected_printer = self._automatic_printer
|
||||
self.selectedPrinterChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariant", notify=selectedPrinterChanged)
|
||||
def selectedPrinterName(self):
|
||||
return self._selected_printer.get("unique_name", "")
|
||||
|
||||
def getPrintJobsUrl(self):
|
||||
return self._host + "/print_jobs"
|
||||
|
||||
def getPrintersUrl(self):
|
||||
return self._host + "/printers"
|
||||
|
||||
def _showProgressMessage(self):
|
||||
progress_message_template = i18n_catalog.i18nc("@info:progress",
|
||||
"Sending <filename>{file_name}</filename> to group {cluster_name}")
|
||||
file_name = os.path.basename(self._file_name).split(".")[0]
|
||||
self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1)
|
||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
|
||||
self._progress_message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||
self._progress_message.show()
|
||||
|
||||
def _addUserAgentHeader(self, request):
|
||||
request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin")
|
||||
|
||||
def _cleanupRequest(self):
|
||||
self._request = None
|
||||
self._stage = OutputStage.ready
|
||||
self._file_name = None
|
||||
|
||||
def _onFinished(self, reply):
|
||||
super()._onFinished(reply)
|
||||
reply_url = reply.url().toString()
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status_code == 500:
|
||||
Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url))
|
||||
return
|
||||
if reply.error() == QNetworkReply.ContentOperationNotPermittedError:
|
||||
# It was probably "/api/v1/materials" for legacy UM3
|
||||
return
|
||||
if reply.error() == QNetworkReply.ContentNotFoundError:
|
||||
# It was probably "/api/v1/print_job" for legacy UM3
|
||||
return
|
||||
|
||||
if reply.operation() == QNetworkAccessManager.PostOperation:
|
||||
if self._cluster_api_prefix + "print_jobs" in reply_url:
|
||||
self._finishedPrintJobPostRequest(reply)
|
||||
return
|
||||
|
||||
# We need to do this check *after* we process the post operation!
|
||||
# If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this.
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error())
|
||||
return
|
||||
|
||||
elif reply.operation() == QNetworkAccessManager.GetOperation:
|
||||
if self._cluster_api_prefix + "print_jobs" in reply_url:
|
||||
self._finishedPrintJobsRequest(reply)
|
||||
elif self._cluster_api_prefix + "printers" in reply_url:
|
||||
self._finishedPrintersRequest(reply)
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrintJobControlPanel(self):
|
||||
Logger.log("d", "Opening print job control panel...")
|
||||
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrinterControlPanel(self):
|
||||
Logger.log("d", "Opening printer control panel...")
|
||||
QDesktopServices.openUrl(QUrl(self.getPrintersUrl()))
|
||||
|
||||
def _onMessageActionTriggered(self, message, action):
|
||||
if action == "open_browser":
|
||||
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
|
||||
|
||||
if action == "Abort":
|
||||
Logger.log("d", "User aborted sending print to remote.")
|
||||
self._progress_message.hide()
|
||||
self._compressing_print = False
|
||||
if self._reply:
|
||||
self._reply.abort()
|
||||
self._stage = OutputStage.ready
|
||||
Application.getInstance().getController().setActiveStage("PrepareStage")
|
||||
|
||||
@pyqtSlot(int, result=str)
|
||||
def formatDuration(self, seconds):
|
||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
||||
|
||||
## For cluster below
|
||||
def _get_plugin_directory_name(self):
|
||||
current_file_absolute_path = os.path.realpath(__file__)
|
||||
directory_path = os.path.dirname(current_file_absolute_path)
|
||||
_, directory_name = os.path.split(directory_path)
|
||||
return directory_name
|
||||
|
||||
@property
|
||||
def _plugin_path(self):
|
||||
return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())
|
File diff suppressed because it is too large
Load diff
|
@ -1,361 +0,0 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import time
|
||||
import json
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore
|
||||
|
||||
from . import NetworkPrinterOutputDevice, NetworkClusterPrinterOutputDevice
|
||||
|
||||
|
||||
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
|
||||
# Zero-Conf is used to detect printers, which are saved in a dict.
|
||||
# If we discover a printer that has the same key as the active machine instance a connection is made.
|
||||
@signalemitter
|
||||
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._zero_conf = None
|
||||
self._browser = None
|
||||
self._printers = {}
|
||||
self._cluster_printers_seen = {} # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer
|
||||
|
||||
self._api_version = "1"
|
||||
self._api_prefix = "/api/v" + self._api_version + "/"
|
||||
self._cluster_api_version = "1"
|
||||
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
|
||||
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
self._network_manager.finished.connect(self._onNetworkRequestFinished)
|
||||
|
||||
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
|
||||
# authentication requests.
|
||||
self._old_printers = []
|
||||
self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
|
||||
|
||||
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
self.addPrinterSignal.connect(self.addPrinter)
|
||||
self.removePrinterSignal.connect(self.removePrinter)
|
||||
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
|
||||
|
||||
# Get list of manual printers from preferences
|
||||
self._preferences = Preferences.getInstance()
|
||||
self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames
|
||||
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
|
||||
|
||||
self._network_requests_buffer = {} # store api responses until data is complete
|
||||
|
||||
# The zeroconf service changed requests are handled in a separate thread, so we can re-schedule the requests
|
||||
# which fail to get detailed service info.
|
||||
# Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
|
||||
# them up and 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()
|
||||
|
||||
addPrinterSignal = Signal()
|
||||
removePrinterSignal = Signal()
|
||||
printerListChanged = Signal()
|
||||
|
||||
## Start looking for devices on network.
|
||||
def start(self):
|
||||
self.startDiscovery()
|
||||
|
||||
def startDiscovery(self):
|
||||
self.stop()
|
||||
if self._browser:
|
||||
self._browser.cancel()
|
||||
self._browser = None
|
||||
self._old_printers = [printer_name for printer_name in self._printers]
|
||||
self._printers = {}
|
||||
self.printerListChanged.emit()
|
||||
# After network switching, one must make a new instance of Zeroconf
|
||||
# On windows, the instance creation is very fast (unnoticable). Other platforms?
|
||||
self._zero_conf = Zeroconf()
|
||||
self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest])
|
||||
|
||||
# Look for manual instances from preference
|
||||
for address in self._manual_instances:
|
||||
if address:
|
||||
self.addManualPrinter(address)
|
||||
|
||||
def addManualPrinter(self, address):
|
||||
if address not in self._manual_instances:
|
||||
self._manual_instances.append(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
instance_name = "manual:%s" % address
|
||||
properties = {
|
||||
b"name": address.encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"incomplete": b"true"
|
||||
}
|
||||
|
||||
if instance_name not in self._printers:
|
||||
# Add a preliminary printer instance
|
||||
self.addPrinter(instance_name, address, properties)
|
||||
|
||||
self.checkManualPrinter(address)
|
||||
self.checkClusterPrinter(address)
|
||||
|
||||
def removeManualPrinter(self, key, address = None):
|
||||
if key in self._printers:
|
||||
if not address:
|
||||
address = self._printers[key].ipAddress
|
||||
self.removePrinter(key)
|
||||
|
||||
if address in self._manual_instances:
|
||||
self._manual_instances.remove(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
def checkManualPrinter(self, address):
|
||||
# Check if a printer exists at this address
|
||||
# If a printer responds, it will replace the preliminary printer created above
|
||||
# origin=manual is for tracking back the origin of the call
|
||||
url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name")
|
||||
name_request = QNetworkRequest(url)
|
||||
self._network_manager.get(name_request)
|
||||
|
||||
def checkClusterPrinter(self, address):
|
||||
cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster")
|
||||
cluster_request = QNetworkRequest(cluster_url)
|
||||
self._network_manager.get(cluster_request)
|
||||
|
||||
## Handler for all requests that have finished.
|
||||
def _onNetworkRequestFinished(self, reply):
|
||||
reply_url = reply.url().toString()
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
|
||||
if reply.operation() == QNetworkAccessManager.GetOperation:
|
||||
address = reply.url().host()
|
||||
if "origin=manual_name" in reply_url: # Name returned from printer.
|
||||
if status_code == 200:
|
||||
|
||||
try:
|
||||
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
Logger.log("e", "Printer returned invalid JSON.")
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
Logger.log("e", "Printer returned incorrect UTF-8.")
|
||||
return
|
||||
|
||||
if address not in self._network_requests_buffer:
|
||||
self._network_requests_buffer[address] = {}
|
||||
self._network_requests_buffer[address]["system"] = system_info
|
||||
elif "origin=check_cluster" in reply_url:
|
||||
if address not in self._network_requests_buffer:
|
||||
self._network_requests_buffer[address] = {}
|
||||
if status_code == 200:
|
||||
# We know it's a cluster printer
|
||||
Logger.log("d", "Cluster printer detected: [%s]", reply.url())
|
||||
|
||||
try:
|
||||
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
Logger.log("e", "Printer returned invalid JSON.")
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
Logger.log("e", "Printer returned incorrect UTF-8.")
|
||||
return
|
||||
|
||||
self._network_requests_buffer[address]["cluster"] = True
|
||||
self._network_requests_buffer[address]["cluster_size"] = len(cluster_printers_list)
|
||||
else:
|
||||
Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url())
|
||||
self._network_requests_buffer[address]["cluster"] = False
|
||||
|
||||
# Both the system call and cluster call are finished
|
||||
if (address in self._network_requests_buffer and
|
||||
"system" in self._network_requests_buffer[address] and
|
||||
"cluster" in self._network_requests_buffer[address]):
|
||||
|
||||
instance_name = "manual:%s" % address
|
||||
system_info = self._network_requests_buffer[address]["system"]
|
||||
machine = "unknown"
|
||||
if "variant" in system_info:
|
||||
variant = system_info["variant"]
|
||||
if variant == "Ultimaker 3":
|
||||
machine = "9066"
|
||||
elif variant == "Ultimaker 3 Extended":
|
||||
machine = "9511"
|
||||
|
||||
properties = {
|
||||
b"name": system_info["name"].encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"firmware_version": system_info["firmware"].encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"machine": machine.encode("utf-8")
|
||||
}
|
||||
|
||||
if self._network_requests_buffer[address]["cluster"]:
|
||||
properties[b"cluster_size"] = self._network_requests_buffer[address]["cluster_size"]
|
||||
|
||||
if instance_name in self._printers:
|
||||
# Only replace the printer if it is still in the list of (manual) printers
|
||||
self.removePrinter(instance_name)
|
||||
self.addPrinter(instance_name, address, properties)
|
||||
|
||||
del self._network_requests_buffer[address]
|
||||
|
||||
## Stop looking for devices on network.
|
||||
def stop(self):
|
||||
if self._zero_conf is not None:
|
||||
Logger.log("d", "zeroconf close...")
|
||||
self._zero_conf.close()
|
||||
|
||||
def getPrinters(self):
|
||||
return self._printers
|
||||
|
||||
def reCheckConnections(self):
|
||||
active_machine = Application.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
||||
for key in self._printers:
|
||||
if key == active_machine.getMetaDataEntry("um_network_key"):
|
||||
if not self._printers[key].isConnected():
|
||||
Logger.log("d", "Connecting [%s]..." % key)
|
||||
self._printers[key].connect()
|
||||
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||
else:
|
||||
if self._printers[key].isConnected():
|
||||
Logger.log("d", "Closing connection [%s]..." % key)
|
||||
self._printers[key].close()
|
||||
self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
|
||||
|
||||
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
def addPrinter(self, name, address, properties):
|
||||
cluster_size = int(properties.get(b"cluster_size", -1))
|
||||
if cluster_size >= 0:
|
||||
printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice(
|
||||
name, address, properties, self._api_prefix)
|
||||
else:
|
||||
printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
|
||||
self._printers[printer.getKey()] = printer
|
||||
self._cluster_printers_seen[printer.getKey()] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||
if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced?
|
||||
Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey())
|
||||
self._printers[printer.getKey()].connect()
|
||||
printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
|
||||
self.printerListChanged.emit()
|
||||
|
||||
def removePrinter(self, name):
|
||||
printer = self._printers.pop(name, None)
|
||||
if printer:
|
||||
if printer.isConnected():
|
||||
printer.disconnect()
|
||||
printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
|
||||
Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
|
||||
self.printerListChanged.emit()
|
||||
|
||||
## Handler for when the connection state of one of the detected printers changes
|
||||
def _onPrinterConnectionStateChanged(self, key):
|
||||
if key not in self._printers:
|
||||
return
|
||||
if self._printers[key].isConnected():
|
||||
self.getOutputDeviceManager().addOutputDevice(self._printers[key])
|
||||
else:
|
||||
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||
|
||||
## Handler for zeroConf detection.
|
||||
# Return True or False indicating if the process succeeded.
|
||||
def _onServiceChanged(self, zeroconf, service_type, name, state_change):
|
||||
if state_change == ServiceStateChange.Added:
|
||||
Logger.log("d", "Bonjour service added: %s" % name)
|
||||
|
||||
# First try getting info from zeroconf cache
|
||||
info = ServiceInfo(service_type, name, properties = {})
|
||||
for record in zeroconf.cache.entries_with_name(name.lower()):
|
||||
info.update_record(zeroconf, time.time(), record)
|
||||
|
||||
for record in zeroconf.cache.entries_with_name(info.server):
|
||||
info.update_record(zeroconf, time.time(), record)
|
||||
if info.address:
|
||||
break
|
||||
|
||||
# Request more data if info is not complete
|
||||
if not info.address:
|
||||
Logger.log("d", "Trying to get address of %s", name)
|
||||
info = zeroconf.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))
|
||||
if address in self._excluded_addresses:
|
||||
Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
|
||||
return False # When getting the localhost IP, then try to reconnect
|
||||
self.addPrinterSignal.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
|
||||
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||
self.removePrinterSignal.emit(str(name))
|
||||
|
||||
return True
|
||||
|
||||
## Appends a service changed request so later the handling thread will pick it up and processes it.
|
||||
def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
|
||||
# append the request and set the event so the event handling thread can pick it up
|
||||
item = (zeroconf, service_type, name, state_change)
|
||||
self._service_changed_request_queue.put(item)
|
||||
self._service_changed_request_event.set()
|
||||
|
||||
def _handleOnServiceChangedRequests(self):
|
||||
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 Application.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)
|
||||
|
||||
@pyqtSlot()
|
||||
def openControlPanel(self):
|
||||
Logger.log("d", "Opening print jobs web UI...")
|
||||
selected_device = self.getOutputDeviceManager().getActiveDevice()
|
||||
if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice):
|
||||
QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))
|
|
@ -15,7 +15,7 @@ Item
|
|||
Label
|
||||
{
|
||||
id: materialLabel
|
||||
text: printCoreConfiguration.material.material + " (" + printCoreConfiguration.material.color + ")"
|
||||
text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("very_small")
|
||||
|
@ -23,7 +23,7 @@ Item
|
|||
Label
|
||||
{
|
||||
id: printCoreLabel
|
||||
text: printCoreConfiguration.print_core_id
|
||||
text: printCoreConfiguration.hotendID
|
||||
anchors.top: materialLabel.bottom
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
|
|
|
@ -20,8 +20,24 @@ UM.Dialog
|
|||
|
||||
visible: true
|
||||
modality: Qt.ApplicationModal
|
||||
onVisibleChanged:
|
||||
{
|
||||
if(visible)
|
||||
{
|
||||
resetPrintersModel()
|
||||
}
|
||||
}
|
||||
title: catalog.i18nc("@title:window", "Print over network")
|
||||
|
||||
title: catalog.i18nc("@title:window","Print over network")
|
||||
property var printersModel: ListModel{}
|
||||
function resetPrintersModel() {
|
||||
printersModel.append({ name: "Automatic", key: ""})
|
||||
|
||||
for (var index in OutputDevice.printers)
|
||||
{
|
||||
printersModel.append({name: OutputDevice.printers[index].name, key: OutputDevice.printers[index].key})
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
|
@ -31,8 +47,7 @@ UM.Dialog
|
|||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
height: 50 * screenScaleFactor
|
||||
|
||||
height: 50 * screenScaleFactord
|
||||
Label
|
||||
{
|
||||
id: manualPrinterSelectionLabel
|
||||
|
@ -42,7 +57,7 @@ UM.Dialog
|
|||
topMargin: UM.Theme.getSize("default_margin").height
|
||||
right: parent.right
|
||||
}
|
||||
text: "Printer selection"
|
||||
text: catalog.i18nc("@label", "Printer selection")
|
||||
wrapMode: Text.Wrap
|
||||
height: 20 * screenScaleFactor
|
||||
}
|
||||
|
@ -50,18 +65,12 @@ UM.Dialog
|
|||
ComboBox
|
||||
{
|
||||
id: printerSelectionCombobox
|
||||
model: OutputDevice.printers
|
||||
textRole: "friendly_name"
|
||||
model: base.printersModel
|
||||
textRole: "name"
|
||||
|
||||
width: parent.width
|
||||
height: 40 * screenScaleFactor
|
||||
Behavior on height { NumberAnimation { duration: 100 } }
|
||||
|
||||
onActivated:
|
||||
{
|
||||
var printerData = OutputDevice.printers[index];
|
||||
OutputDevice.selectPrinter(printerData.unique_name, printerData.friendly_name);
|
||||
}
|
||||
}
|
||||
|
||||
SystemPalette
|
||||
|
@ -79,8 +88,6 @@ UM.Dialog
|
|||
enabled: true
|
||||
onClicked: {
|
||||
base.visible = false;
|
||||
// reset to defaults
|
||||
OutputDevice.selectAutomaticPrinter()
|
||||
printerSelectionCombobox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
@ -93,9 +100,8 @@ UM.Dialog
|
|||
enabled: true
|
||||
onClicked: {
|
||||
base.visible = false;
|
||||
OutputDevice.sendPrintJob();
|
||||
OutputDevice.sendPrintJob(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
|
||||
// reset to defaults
|
||||
OutputDevice.selectAutomaticPrinter()
|
||||
printerSelectionCombobox.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,16 +22,16 @@ Rectangle
|
|||
{
|
||||
return "";
|
||||
}
|
||||
if (printJob.time_total === 0)
|
||||
if (printJob.timeTotal === 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return Math.min(100, Math.round(printJob.time_elapsed / printJob.time_total * 100)) + "%";
|
||||
return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
|
||||
}
|
||||
|
||||
function printerStatusText(printer)
|
||||
{
|
||||
switch (printer.status)
|
||||
switch (printer.state)
|
||||
{
|
||||
case "pre_print":
|
||||
return catalog.i18nc("@label", "Preparing to print")
|
||||
|
@ -49,31 +49,23 @@ Rectangle
|
|||
}
|
||||
|
||||
id: printerDelegate
|
||||
property var printer
|
||||
|
||||
property var printer: null
|
||||
property var printJob: printer != null ? printer.activePrintJob: null
|
||||
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: mouse.containsMouse ? emphasisColor : lineColor
|
||||
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
|
||||
|
||||
property var printJob:
|
||||
{
|
||||
if (printer.reserved_by != null)
|
||||
{
|
||||
// Look in another list.
|
||||
return OutputDevice.printJobsByUUID[printer.reserved_by]
|
||||
}
|
||||
return OutputDevice.printJobsByPrinterUUID[printer.uuid]
|
||||
}
|
||||
|
||||
MouseArea
|
||||
{
|
||||
id: mouse
|
||||
anchors.fill:parent
|
||||
onClicked: OutputDevice.selectPrinter(printer.unique_name, printer.friendly_name)
|
||||
onClicked: OutputDevice.setActivePrinter(printer)
|
||||
hoverEnabled: true;
|
||||
|
||||
// Only clickable if no printer is selected
|
||||
enabled: OutputDevice.selectedPrinterName == "" && printer.status !== "unreachable"
|
||||
enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
|
||||
}
|
||||
|
||||
Row
|
||||
|
@ -122,7 +114,7 @@ Rectangle
|
|||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
text: printJob != null ? getPrettyTime(printJob.time_total) : ""
|
||||
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
|
||||
opacity: 0.65
|
||||
font: UM.Theme.getFont("default")
|
||||
elide: Text.ElideRight
|
||||
|
@ -140,7 +132,7 @@ Rectangle
|
|||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
|
||||
text: printer.friendly_name
|
||||
text: printer.name
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
@ -150,7 +142,7 @@ Rectangle
|
|||
id: printerTypeLabel
|
||||
anchors.top: printerNameLabel.bottom
|
||||
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
|
||||
text: printer.machine_variant
|
||||
text: printer.type
|
||||
anchors.left: parent.left
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("very_small")
|
||||
|
@ -166,7 +158,7 @@ Rectangle
|
|||
anchors.right: printProgressArea.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
color: emphasisColor
|
||||
opacity: printer != null && printer.status === "unreachable" ? 0.3 : 1
|
||||
opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
|
||||
|
||||
Image
|
||||
{
|
||||
|
@ -192,7 +184,7 @@ Rectangle
|
|||
{
|
||||
id: leftExtruderInfo
|
||||
width: Math.floor((parent.width - extruderSeperator.width) / 2)
|
||||
printCoreConfiguration: printer.configuration[0]
|
||||
printCoreConfiguration: printer.extruders[0]
|
||||
}
|
||||
|
||||
Rectangle
|
||||
|
@ -207,7 +199,7 @@ Rectangle
|
|||
{
|
||||
id: rightExtruderInfo
|
||||
width: Math.floor((parent.width - extruderSeperator.width) / 2)
|
||||
printCoreConfiguration: printer.configuration[1]
|
||||
printCoreConfiguration: printer.extruders[1]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,9 +217,9 @@ Rectangle
|
|||
if(printJob != null)
|
||||
{
|
||||
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
|
||||
return extendStates.indexOf(printJob.status) !== -1;
|
||||
return extendStates.indexOf(printJob.state) !== -1;
|
||||
}
|
||||
return !printer.enabled;
|
||||
return printer.state == "disabled"
|
||||
}
|
||||
|
||||
Item // Status and Percent
|
||||
|
@ -235,7 +227,7 @@ Rectangle
|
|||
id: printProgressTitleBar
|
||||
|
||||
property var showPercent: {
|
||||
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.status) !== -1);
|
||||
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
|
@ -252,19 +244,19 @@ Rectangle
|
|||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (!printer.enabled)
|
||||
if (printer.state == "disabled")
|
||||
{
|
||||
return catalog.i18nc("@label:status", "Disabled");
|
||||
}
|
||||
|
||||
if (printer.status === "unreachable")
|
||||
if (printer.state === "unreachable")
|
||||
{
|
||||
return printerStatusText(printer);
|
||||
}
|
||||
|
||||
if (printJob != null)
|
||||
{
|
||||
switch (printJob.status)
|
||||
switch (printJob.state)
|
||||
{
|
||||
case "printing":
|
||||
case "post_print":
|
||||
|
@ -277,14 +269,7 @@ Rectangle
|
|||
case "sent_to_printer":
|
||||
return catalog.i18nc("@label", "Preparing to print")
|
||||
case "queued":
|
||||
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
|
||||
{
|
||||
return catalog.i18nc("@label:status", "Action required");
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
case "pausing":
|
||||
case "paused":
|
||||
return catalog.i18nc("@label:status", "Paused");
|
||||
|
@ -328,26 +313,23 @@ Rectangle
|
|||
visible: !printProgressTitleBar.showPercent
|
||||
|
||||
source: {
|
||||
if (!printer.enabled)
|
||||
if (printer.state == "disabled")
|
||||
{
|
||||
return "blocked-icon.svg";
|
||||
}
|
||||
|
||||
if (printer.status === "unreachable")
|
||||
if (printer.state === "unreachable")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (printJob != null)
|
||||
{
|
||||
if(printJob.status === "queued")
|
||||
if(printJob.state === "queued")
|
||||
{
|
||||
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
|
||||
{
|
||||
return "action-required-icon.svg";
|
||||
}
|
||||
return "action-required-icon.svg";
|
||||
}
|
||||
else if (printJob.status === "wait_cleanup")
|
||||
else if (printJob.state === "wait_cleanup")
|
||||
{
|
||||
return "checkmark-icon.svg";
|
||||
}
|
||||
|
@ -384,23 +366,23 @@ Rectangle
|
|||
{
|
||||
text:
|
||||
{
|
||||
if (!printer.enabled)
|
||||
if (printer.state == "disabled")
|
||||
{
|
||||
return catalog.i18nc("@label", "Not accepting print jobs");
|
||||
}
|
||||
|
||||
if (printer.status === "unreachable")
|
||||
if (printer.state === "unreachable")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if(printJob != null)
|
||||
{
|
||||
switch (printJob.status)
|
||||
switch (printJob.state)
|
||||
{
|
||||
case "printing":
|
||||
case "post_print":
|
||||
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.time_total - printJob.time_elapsed)
|
||||
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
|
||||
case "wait_cleanup":
|
||||
return catalog.i18nc("@label", "Clear build plate")
|
||||
case "sent_to_printer":
|
||||
|
@ -409,10 +391,7 @@ Rectangle
|
|||
case "wait_for_configuration":
|
||||
return catalog.i18nc("@label", "Not accepting print jobs")
|
||||
case "queued":
|
||||
if (printJob.configuration_changes_required != undefined)
|
||||
{
|
||||
return catalog.i18nc("@label", "Waiting for configuration change");
|
||||
}
|
||||
return catalog.i18nc("@label", "Waiting for configuration change");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -432,7 +411,7 @@ Rectangle
|
|||
text: {
|
||||
if(printJob != null)
|
||||
{
|
||||
if(printJob.status == "printing" || printJob.status == "post_print")
|
||||
if(printJob.state == "printing" || printJob.state == "post_print")
|
||||
{
|
||||
return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ Item
|
|||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
onClicked: OutputDevice.selectAutomaticPrinter()
|
||||
onClicked: OutputDevice.setActivePrinter(null)
|
||||
z: 0
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ Item
|
|||
width: 20 * screenScaleFactor
|
||||
height: 20 * screenScaleFactor
|
||||
|
||||
onClicked: OutputDevice.selectAutomaticPrinter()
|
||||
onClicked: OutputDevice.setActivePrinter(null)
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
|
@ -65,17 +65,23 @@ Item
|
|||
{
|
||||
if(visible)
|
||||
{
|
||||
OutputDevice.startCamera()
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.start()
|
||||
}
|
||||
} else
|
||||
{
|
||||
OutputDevice.stopCamera()
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
|
||||
{
|
||||
OutputDevice.activePrinter.camera.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
source:
|
||||
{
|
||||
if(OutputDevice.cameraImage)
|
||||
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
|
||||
{
|
||||
return OutputDevice.cameraImage;
|
||||
return OutputDevice.activePrinter.camera.latestImage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ Item
|
|||
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
|
||||
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
|
||||
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
||||
property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested
|
||||
property bool authenticationRequested: printerConnected && (Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 || Cura.MachineManager.printerOutputDevices[0].authenticationState == 5) // AuthState.AuthenticationRequested or AuthenticationReceived.
|
||||
|
||||
Row
|
||||
{
|
||||
|
@ -119,7 +119,9 @@ Item
|
|||
onClicked: manager.loadConfigurationFromPrinter()
|
||||
|
||||
function isClusterPrinter() {
|
||||
if(Cura.MachineManager.printerOutputDevices.length == 0)
|
||||
return false
|
||||
//TODO: Hardcoded this for the moment now. These info components might also need to move.
|
||||
/*if(Cura.MachineManager.printerOutputDevices.length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -129,7 +131,7 @@ Item
|
|||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return true;*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
332
plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
Normal file
332
plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
Normal file
|
@ -0,0 +1,332 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from UM.Logger import Logger
|
||||
from UM.Application import Application
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Version import Version
|
||||
|
||||
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from time import time
|
||||
|
||||
import json
|
||||
|
||||
|
||||
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
|
||||
# Zero-Conf is used to detect printers, which are saved in a dict.
|
||||
# If we discover a printer that has the same key as the active machine instance a connection is made.
|
||||
@signalemitter
|
||||
class UM3OutputDevicePlugin(OutputDevicePlugin):
|
||||
addDeviceSignal = Signal()
|
||||
removeDeviceSignal = Signal()
|
||||
discoveredDevicesChanged = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._zero_conf = None
|
||||
self._zero_conf_browser = None
|
||||
|
||||
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
self.addDeviceSignal.connect(self._onAddDevice)
|
||||
self.removeDeviceSignal.connect(self._onRemoveDevice)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
|
||||
|
||||
self._discovered_devices = {}
|
||||
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
self._network_manager.finished.connect(self._onNetworkRequestFinished)
|
||||
|
||||
self._min_cluster_version = Version("4.0.0")
|
||||
|
||||
self._api_version = "1"
|
||||
self._api_prefix = "/api/v" + self._api_version + "/"
|
||||
self._cluster_api_version = "1"
|
||||
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
|
||||
|
||||
# Get list of manual instances from preferences
|
||||
self._preferences = Preferences.getInstance()
|
||||
self._preferences.addPreference("um3networkprinting/manual_instances",
|
||||
"") # A comma-separated list of ip adresses or hostnames
|
||||
|
||||
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
|
||||
|
||||
# The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
|
||||
# which fail to get detailed service info.
|
||||
# Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
|
||||
# them up and 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()
|
||||
|
||||
def getDiscoveredDevices(self):
|
||||
return self._discovered_devices
|
||||
|
||||
## Start looking for devices on network.
|
||||
def start(self):
|
||||
self.startDiscovery()
|
||||
|
||||
def startDiscovery(self):
|
||||
self.stop()
|
||||
if self._zero_conf_browser:
|
||||
self._zero_conf_browser.cancel()
|
||||
self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
|
||||
|
||||
self._zero_conf = Zeroconf()
|
||||
self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
|
||||
[self._appendServiceChangedRequest])
|
||||
|
||||
# Look for manual instances from preference
|
||||
for address in self._manual_instances:
|
||||
if address:
|
||||
self.addManualDevice(address)
|
||||
|
||||
def reCheckConnections(self):
|
||||
active_machine = Application.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)
|
||||
self._discovered_devices[key].connect()
|
||||
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
|
||||
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)
|
||||
|
||||
def _onDeviceConnectionStateChanged(self, key):
|
||||
if key not in self._discovered_devices:
|
||||
return
|
||||
if self._discovered_devices[key].isConnected():
|
||||
self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
|
||||
else:
|
||||
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||
|
||||
def stop(self):
|
||||
if self._zero_conf is not None:
|
||||
Logger.log("d", "zeroconf close...")
|
||||
self._zero_conf.close()
|
||||
|
||||
def removeManualDevice(self, key, address = None):
|
||||
if key in self._discovered_devices:
|
||||
if not address:
|
||||
address = self._printers[key].ipAddress
|
||||
self._onRemoveDevice(key)
|
||||
|
||||
if address in self._manual_instances:
|
||||
self._manual_instances.remove(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
def addManualDevice(self, address):
|
||||
if address not in self._manual_instances:
|
||||
self._manual_instances.append(address)
|
||||
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
|
||||
|
||||
instance_name = "manual:%s" % address
|
||||
properties = {
|
||||
b"name": address.encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"incomplete": b"true"
|
||||
}
|
||||
|
||||
if instance_name not in self._discovered_devices:
|
||||
# Add a preliminary printer instance
|
||||
self._onAddDevice(instance_name, address, properties)
|
||||
|
||||
self._checkManualDevice(address)
|
||||
|
||||
def _checkManualDevice(self, address):
|
||||
# Check if a UM3 family device exists at this address.
|
||||
# If a printer responds, it will replace the preliminary printer created above
|
||||
# origin=manual is for tracking back the origin of the call
|
||||
url = QUrl("http://" + address + self._api_prefix + "system")
|
||||
name_request = QNetworkRequest(url)
|
||||
self._network_manager.get(name_request)
|
||||
|
||||
def _onNetworkRequestFinished(self, reply):
|
||||
reply_url = reply.url().toString()
|
||||
|
||||
if "system" in reply_url:
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
# Something went wrong with checking the firmware version!
|
||||
return
|
||||
|
||||
try:
|
||||
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except:
|
||||
Logger.log("e", "Something went wrong converting the JSON.")
|
||||
return
|
||||
|
||||
address = reply.url().host()
|
||||
has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
|
||||
instance_name = "manual:%s" % address
|
||||
properties = {
|
||||
b"name": system_info["name"].encode("utf-8"),
|
||||
b"address": address.encode("utf-8"),
|
||||
b"firmware_version": system_info["firmware"].encode("utf-8"),
|
||||
b"manual": b"true",
|
||||
b"machine": system_info["variant"].encode("utf-8")
|
||||
}
|
||||
|
||||
if has_cluster_capable_firmware:
|
||||
# Cluster needs an additional request, before it's completed.
|
||||
properties[b"incomplete"] = b"true"
|
||||
|
||||
# Check if the device is still in the list & re-add it with the updated
|
||||
# information.
|
||||
if instance_name in self._discovered_devices:
|
||||
self._onRemoveDevice(instance_name)
|
||||
self._onAddDevice(instance_name, address, properties)
|
||||
|
||||
if has_cluster_capable_firmware:
|
||||
# We need to request more info in order to figure out the size of the cluster.
|
||||
cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
|
||||
cluster_request = QNetworkRequest(cluster_url)
|
||||
self._network_manager.get(cluster_request)
|
||||
|
||||
elif "printers" in reply_url:
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
# Something went wrong with checking the amount of printers the cluster has!
|
||||
return
|
||||
# So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
|
||||
try:
|
||||
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
except:
|
||||
Logger.log("e", "Something went wrong converting the JSON.")
|
||||
return
|
||||
address = reply.url().host()
|
||||
instance_name = "manual:%s" % address
|
||||
if instance_name in self._discovered_devices:
|
||||
device = self._discovered_devices[instance_name]
|
||||
properties = device.getProperties().copy()
|
||||
if b"incomplete" in properties:
|
||||
del properties[b"incomplete"]
|
||||
properties[b'cluster_size'] = len(cluster_printers_list)
|
||||
self._onRemoveDevice(instance_name)
|
||||
self._onAddDevice(instance_name, address, properties)
|
||||
|
||||
def _onRemoveDevice(self, device_id):
|
||||
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
|
||||
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
def _onAddDevice(self, name, address, properties):
|
||||
# Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
|
||||
# or "Legacy" UM3 device.
|
||||
cluster_size = int(properties.get(b"cluster_size", -1))
|
||||
|
||||
if cluster_size >= 0:
|
||||
device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
|
||||
else:
|
||||
device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)
|
||||
|
||||
self._discovered_devices[device.getId()] = device
|
||||
self.discoveredDevicesChanged.emit()
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
|
||||
device.connect()
|
||||
device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
|
||||
|
||||
## Appends a service changed request so later the handling thread will pick it up and processes it.
|
||||
def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
|
||||
# append the request and set the event so the event handling thread can pick it up
|
||||
item = (zeroconf, service_type, name, state_change)
|
||||
self._service_changed_request_queue.put(item)
|
||||
self._service_changed_request_event.set()
|
||||
|
||||
def _handleOnServiceChangedRequests(self):
|
||||
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 Application.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)
|
||||
|
||||
## 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 carefull calling it from the main thread.
|
||||
def _onServiceChanged(self, zero_conf, service_type, name, state_change):
|
||||
if state_change == ServiceStateChange.Added:
|
||||
Logger.log("d", "Bonjour service added: %s" % name)
|
||||
|
||||
# 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:
|
||||
Logger.log("d", "Trying to get address of %s", name)
|
||||
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.addDeviceSignal.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
|
||||
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
Logger.log("d", "Bonjour service removed: %s" % name)
|
||||
self.removeDeviceSignal.emit(str(name))
|
||||
|
||||
return True
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from . import NetworkPrinterOutputDevicePlugin
|
||||
|
||||
from . import DiscoverUM3Action
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from . import UM3OutputDevicePlugin
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
||||
return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
66
plugins/USBPrinting/AutoDetectBaudJob.py
Normal file
66
plugins/USBPrinting/AutoDetectBaudJob.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
|
||||
from .avr_isp.stk500v2 import Stk500v2
|
||||
|
||||
from time import time, sleep
|
||||
from serial import Serial, SerialException
|
||||
|
||||
|
||||
# An async job that attempts to find the correct baud rate for a USB printer.
|
||||
# It tries a pre-set list of baud rates. All these baud rates are validated by requesting the temperature a few times
|
||||
# and checking if the results make sense. If getResult() is not None, it was able to find a correct baud rate.
|
||||
class AutoDetectBaudJob(Job):
|
||||
def __init__(self, serial_port):
|
||||
super().__init__()
|
||||
self._serial_port = serial_port
|
||||
self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
|
||||
|
||||
def run(self):
|
||||
Logger.log("d", "Auto detect baud rate started.")
|
||||
timeout = 3
|
||||
|
||||
programmer = Stk500v2()
|
||||
serial = None
|
||||
try:
|
||||
programmer.connect(self._serial_port)
|
||||
serial = programmer.leaveISP()
|
||||
except:
|
||||
programmer.close()
|
||||
|
||||
for baud_rate in self._all_baud_rates:
|
||||
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
|
||||
|
||||
if serial is None:
|
||||
try:
|
||||
serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout)
|
||||
except SerialException as e:
|
||||
Logger.logException("w", "Unable to create serial")
|
||||
continue
|
||||
else:
|
||||
# We already have a serial connection, just change the baud rate.
|
||||
try:
|
||||
serial.baudrate = baud_rate
|
||||
except:
|
||||
continue
|
||||
sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
|
||||
successful_responses = 0
|
||||
|
||||
serial.write(b"\n") # Ensure we clear out previous responses
|
||||
serial.write(b"M105\n")
|
||||
|
||||
timeout_time = time() + timeout
|
||||
|
||||
while timeout_time > time():
|
||||
line = serial.readline()
|
||||
if b"ok T:" in line:
|
||||
successful_responses += 1
|
||||
if successful_responses >= 3:
|
||||
self.setResult(baud_rate)
|
||||
return
|
||||
|
||||
serial.write(b"M105\n")
|
||||
self.setResult(None) # Unable to detect the correct baudrate.
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Copyright (c) 2017 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
|
@ -34,44 +34,22 @@ UM.Dialog
|
|||
}
|
||||
|
||||
text: {
|
||||
if (manager.errorCode == 0)
|
||||
switch (manager.firmwareUpdateState)
|
||||
{
|
||||
if (manager.firmwareUpdateCompleteStatus)
|
||||
{
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update completed.")
|
||||
}
|
||||
else if (manager.progress == 0)
|
||||
{
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Starting firmware update, this may take a while.")
|
||||
}
|
||||
else
|
||||
{
|
||||
//: Firmware update status label
|
||||
case 0:
|
||||
return "" //Not doing anything (eg; idling)
|
||||
case 1:
|
||||
return catalog.i18nc("@label","Updating firmware.")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (manager.errorCode)
|
||||
{
|
||||
case 1:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
|
||||
case 2:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
|
||||
case 3:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
|
||||
case 4:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
|
||||
default:
|
||||
//: Firmware update status label
|
||||
return catalog.i18nc("@label", "Unknown error code: %1").arg(manager.errorCode)
|
||||
}
|
||||
case 2:
|
||||
return catalog.i18nc("@label","Firmware update completed.")
|
||||
case 3:
|
||||
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
|
||||
case 4:
|
||||
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
|
||||
case 5:
|
||||
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
|
||||
case 6:
|
||||
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,16 +59,15 @@ UM.Dialog
|
|||
ProgressBar
|
||||
{
|
||||
id: prog
|
||||
value: manager.firmwareUpdateCompleteStatus ? 100 : manager.progress
|
||||
value: manager.firmwareProgress
|
||||
minimumValue: 0
|
||||
maximumValue: 100
|
||||
indeterminate: (manager.progress < 1) && (!manager.firmwareUpdateCompleteStatus)
|
||||
indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
|
||||
anchors
|
||||
{
|
||||
left: parent.left;
|
||||
right: parent.right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SystemPalette
|
||||
|
|
68
plugins/USBPrinting/USBPrinterOutputController.py
Normal file
68
plugins/USBPrinting/USBPrinterOutputController.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class USBPrinterOuptutController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
super().__init__(output_device)
|
||||
|
||||
self._preheat_bed_timer = QTimer()
|
||||
self._preheat_bed_timer.setSingleShot(True)
|
||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
||||
self._preheat_printer = None
|
||||
|
||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
||||
self._output_device.sendCommand("G91")
|
||||
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
||||
self._output_device.sendCommand("G90")
|
||||
|
||||
def homeHead(self, printer):
|
||||
self._output_device.sendCommand("G28 X")
|
||||
self._output_device.sendCommand("G28 Y")
|
||||
|
||||
def homeBed(self, printer):
|
||||
self._output_device.sendCommand("G28 Z")
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
if state == "pause":
|
||||
self._output_device.pausePrint()
|
||||
job.updateState("paused")
|
||||
elif state == "print":
|
||||
self._output_device.resumePrint()
|
||||
job.updateState("printing")
|
||||
elif state == "abort":
|
||||
self._output_device.cancelPrint()
|
||||
pass
|
||||
|
||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||
try:
|
||||
temperature = round(temperature) # The API doesn't allow floating point.
|
||||
duration = round(duration)
|
||||
except ValueError:
|
||||
return # Got invalid values, can't pre-heat.
|
||||
|
||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
||||
self._preheat_bed_timer.start()
|
||||
self._preheat_printer = printer
|
||||
printer.updateIsPreheating(True)
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||
self.preheatBed(printer, temperature=0, duration=0)
|
||||
self._preheat_bed_timer.stop()
|
||||
printer.updateIsPreheating(False)
|
||||
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||
self._output_device.sendCommand("M140 S%s" % temperature)
|
||||
|
||||
def _onPreheatBedTimerFinished(self):
|
||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||
self._preheat_printer.updateIsPreheating(False)
|
File diff suppressed because it is too large
Load diff
|
@ -2,33 +2,32 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from . import USBPrinterOutputDevice
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from cura.PrinterOutputDevice import ConnectionState
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from cura.PrinterOutputDevice import ConnectionState
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from . import USBPrinterOutputDevice
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
import threading
|
||||
import platform
|
||||
import time
|
||||
import os.path
|
||||
import serial.tools.list_ports
|
||||
from UM.Extension import Extension
|
||||
|
||||
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer.
|
||||
## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
|
||||
@signalemitter
|
||||
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
||||
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
|
||||
addUSBOutputDeviceSignal = Signal()
|
||||
progressChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent = parent)
|
||||
self._serial_port_list = []
|
||||
|
@ -38,39 +37,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
self._update_thread.setDaemon(True)
|
||||
|
||||
self._check_updates = True
|
||||
self._firmware_view = None
|
||||
|
||||
Application.getInstance().applicationShuttingDown.connect(self.stop)
|
||||
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
|
||||
addUSBOutputDeviceSignal = Signal()
|
||||
connectionStateChanged = pyqtSignal()
|
||||
|
||||
progressChanged = pyqtSignal()
|
||||
firmwareUpdateChange = pyqtSignal()
|
||||
|
||||
@pyqtProperty(float, notify = progressChanged)
|
||||
def progress(self):
|
||||
progress = 0
|
||||
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
|
||||
progress += device.progress
|
||||
return progress / len(self._usb_output_devices)
|
||||
|
||||
@pyqtProperty(int, notify = progressChanged)
|
||||
def errorCode(self):
|
||||
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
|
||||
if device._error_code:
|
||||
return device._error_code
|
||||
return 0
|
||||
|
||||
## Return True if all printers finished firmware update
|
||||
@pyqtProperty(float, notify = firmwareUpdateChange)
|
||||
def firmwareUpdateCompleteStatus(self):
|
||||
complete = True
|
||||
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
|
||||
if not device.firmwareUpdateFinished:
|
||||
complete = False
|
||||
return complete
|
||||
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
|
||||
|
||||
def start(self):
|
||||
self._check_updates = True
|
||||
|
@ -79,58 +49,28 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
def stop(self):
|
||||
self._check_updates = False
|
||||
|
||||
def _updateThread(self):
|
||||
while self._check_updates:
|
||||
result = self.getSerialPortList(only_list_usb = True)
|
||||
self._addRemovePorts(result)
|
||||
time.sleep(5)
|
||||
|
||||
## Show firmware interface.
|
||||
# This will create the view if its not already created.
|
||||
def spawnFirmwareInterface(self, serial_port):
|
||||
if self._firmware_view is None:
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
|
||||
self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
|
||||
self._firmware_view.show()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateAllFirmware(self, file_name):
|
||||
if file_name.startswith("file://"):
|
||||
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
|
||||
|
||||
if not self._usb_output_devices:
|
||||
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
|
||||
def _onConnectionStateChanged(self, serial_port):
|
||||
if serial_port not in self._usb_output_devices:
|
||||
return
|
||||
|
||||
for printer_connection in self._usb_output_devices:
|
||||
self._usb_output_devices[printer_connection].resetFirmwareUpdate()
|
||||
self.spawnFirmwareInterface("")
|
||||
for printer_connection in self._usb_output_devices:
|
||||
try:
|
||||
self._usb_output_devices[printer_connection].updateFirmware(file_name)
|
||||
except FileNotFoundError:
|
||||
# Should only happen in dev environments where the resources/firmware folder is absent.
|
||||
self._usb_output_devices[printer_connection].setProgress(100, 100)
|
||||
Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name)
|
||||
Message(i18n_catalog.i18nc("@info",
|
||||
"Could not find firmware required for the printer at %s.") % printer_connection, title = i18n_catalog.i18nc("@info:title", "Printer Firmware")).show()
|
||||
self._firmware_view.close()
|
||||
changed_device = self._usb_output_devices[serial_port]
|
||||
if changed_device.connectionState == ConnectionState.connected:
|
||||
self.getOutputDeviceManager().addOutputDevice(changed_device)
|
||||
else:
|
||||
self.getOutputDeviceManager().removeOutputDevice(serial_port)
|
||||
|
||||
def _updateThread(self):
|
||||
while self._check_updates:
|
||||
container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if container_stack is None:
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def updateFirmwareBySerial(self, serial_port, file_name):
|
||||
if serial_port in self._usb_output_devices:
|
||||
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
|
||||
try:
|
||||
self._usb_output_devices[serial_port].updateFirmware(file_name)
|
||||
except FileNotFoundError:
|
||||
self._firmware_view.close()
|
||||
Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name)
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
if container_stack.getMetaDataEntry("supports_usb_connection"):
|
||||
port_list = self.getSerialPortList(only_list_usb=True)
|
||||
else:
|
||||
port_list = [] # Just use an empty list; all USB devices will be removed.
|
||||
self._addRemovePorts(port_list)
|
||||
time.sleep(5)
|
||||
|
||||
## Return the singleton instance of the USBPrinterManager
|
||||
@classmethod
|
||||
|
@ -191,7 +131,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
Logger.log("w", "There is no firmware for machine %s.", machine_id)
|
||||
|
||||
if hex_file:
|
||||
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
|
||||
try:
|
||||
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
|
||||
except FileNotFoundError:
|
||||
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
|
||||
return ""
|
||||
else:
|
||||
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
|
||||
return ""
|
||||
|
@ -205,46 +149,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
continue
|
||||
self._serial_port_list = list(serial_ports)
|
||||
|
||||
devices_to_remove = []
|
||||
for port, device in self._usb_output_devices.items():
|
||||
if port not in self._serial_port_list:
|
||||
device.close()
|
||||
devices_to_remove.append(port)
|
||||
|
||||
for port in devices_to_remove:
|
||||
del self._usb_output_devices[port]
|
||||
|
||||
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
|
||||
def addOutputDevice(self, serial_port):
|
||||
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
|
||||
device.connectionStateChanged.connect(self._onConnectionStateChanged)
|
||||
device.connect()
|
||||
device.progressChanged.connect(self.progressChanged)
|
||||
device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
|
||||
self._usb_output_devices[serial_port] = device
|
||||
|
||||
## If one of the states of the connected devices change, we might need to add / remove them from the global list.
|
||||
def _onConnectionStateChanged(self, serial_port):
|
||||
success = True
|
||||
try:
|
||||
if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
|
||||
self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
|
||||
else:
|
||||
success = success and self.getOutputDeviceManager().removeOutputDevice(serial_port)
|
||||
if success:
|
||||
self.connectionStateChanged.emit()
|
||||
except KeyError:
|
||||
Logger.log("w", "Connection state of %s changed, but it was not found in the list")
|
||||
|
||||
@pyqtProperty(QObject , notify = connectionStateChanged)
|
||||
def connectedPrinterList(self):
|
||||
self._usb_output_devices_model = ListModel()
|
||||
self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
|
||||
self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
|
||||
for connection in self._usb_output_devices:
|
||||
if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
|
||||
self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
|
||||
return self._usb_output_devices_model
|
||||
device.connect()
|
||||
|
||||
## Create a list of serial ports on the system.
|
||||
# \param only_list_usb If true, only usb ports are listed
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import USBPrinterOutputDeviceManager
|
||||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
|
||||
from PyQt5.QtQml import qmlRegisterSingletonType
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
# We are violating the QT API here (as we use a factory, which is technically not allowed).
|
||||
# but we don't really have another means for doing this (and it seems to you know -work-)
|
||||
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
|
||||
return {"extension":USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance(), "output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}
|
||||
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}
|
||||
|
|
|
@ -14,6 +14,9 @@ import Cura 1.0 as Cura
|
|||
Cura.MachineAction
|
||||
{
|
||||
anchors.fill: parent;
|
||||
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
|
||||
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
|
||||
|
||||
Item
|
||||
{
|
||||
id: upgradeFirmwareMachineAction
|
||||
|
@ -60,16 +63,17 @@ Cura.MachineAction
|
|||
{
|
||||
id: autoUpgradeButton
|
||||
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
|
||||
enabled: parent.firmwareName != ""
|
||||
enabled: parent.firmwareName != "" && activeOutputDevice
|
||||
onClicked:
|
||||
{
|
||||
Cura.USBPrinterManager.updateAllFirmware(parent.firmwareName)
|
||||
activeOutputDevice.updateFirmware(parent.firmwareName)
|
||||
}
|
||||
}
|
||||
Button
|
||||
{
|
||||
id: manualUpgradeButton
|
||||
text: catalog.i18nc("@action:button", "Upload custom Firmware");
|
||||
enabled: activeOutputDevice != null
|
||||
onClicked:
|
||||
{
|
||||
customFirmwareDialog.open()
|
||||
|
@ -83,7 +87,7 @@ Cura.MachineAction
|
|||
title: catalog.i18nc("@title:window", "Select custom firmware")
|
||||
nameFilters: "Firmware image files (*.hex)"
|
||||
selectExisting: true
|
||||
onAccepted: Cura.USBPrinterManager.updateAllFirmware(fileUrl)
|
||||
onAccepted: activeOutputDevice.updateFirmware(fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
from . import BedLevelMachineAction
|
||||
from . import UpgradeFirmwareMachineAction
|
||||
from . import UMOCheckupMachineAction
|
||||
from . import UMOUpgradeSelection
|
||||
from . import UM2UpgradeSelection
|
||||
|
||||
|
@ -18,7 +17,6 @@ def register(app):
|
|||
return { "machine_action": [
|
||||
BedLevelMachineAction.BedLevelMachineAction(),
|
||||
UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(),
|
||||
UMOCheckupMachineAction.UMOCheckupMachineAction(),
|
||||
UMOUpgradeSelection.UMOUpgradeSelection(),
|
||||
UM2UpgradeSelection.UM2UpgradeSelection()
|
||||
]}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue