From e35fba6f05d5d86a47db7a1f7d26138445a2155e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Nov 2017 15:05:08 +0100 Subject: [PATCH 001/135] Added first stubs for printer output models CL-541 --- cura/PrinterOutput/ExtruderModel.py | 76 ++++++++++++++++++ cura/PrinterOutput/MaterialModel.py | 29 +++++++ cura/PrinterOutput/PrintJobModel.py | 10 +++ cura/PrinterOutput/PrinterModel.py | 115 ++++++++++++++++++++++++++++ cura/PrinterOutput/__init__.py | 0 5 files changed, 230 insertions(+) create mode 100644 cura/PrinterOutput/ExtruderModel.py create mode 100644 cura/PrinterOutput/MaterialModel.py create mode 100644 cura/PrinterOutput/PrintJobModel.py create mode 100644 cura/PrinterOutput/PrinterModel.py create mode 100644 cura/PrinterOutput/__init__.py diff --git a/cura/PrinterOutput/ExtruderModel.py b/cura/PrinterOutput/ExtruderModel.py new file mode 100644 index 0000000000..f08b21aaac --- /dev/null +++ b/cura/PrinterOutput/ExtruderModel.py @@ -0,0 +1,76 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot +from UM.Logger import Logger + +from typing import Optional + +MYPY = False +if MYPY: + from cura.PrinterOutput.PrinterModel import PrinterModel + from cura.PrinterOutput.MaterialModel import MaterialModel + + +class ExtruderModel(QObject): + hotendIDChanged = pyqtSignal() + targetHotendTemperatureChanged = pyqtSignal() + hotendTemperatureChanged = pyqtSignal() + activeMaterialChanged = pyqtSignal() + + def __init__(self, printer: "PrinterModel", parent=None): + super().__init__(parent) + self._printer = printer + self._target_hotend_temperature = 0 + self._hotend_temperature = 0 + self._hotend_id = "" + self._active_material = None # type: Optional[MaterialModel] + + @pyqtProperty(QObject, notify = activeMaterialChanged) + def activeMaterial(self) -> "MaterialModel": + return self._active_material + + def updateActiveMaterial(self, material: Optional["MaterialModel"]): + if self._active_material != material: + self._active_material = material + self.activeMaterialChanged.emit() + + ## Update the hotend temperature. This only changes it locally. + def updateHotendTemperature(self, temperature: int): + if self._hotend_temperature != temperature: + self._hotend_temperature = temperature + self.hotendTemperatureChanged.emit() + + def updateTargetHotendTemperature(self, temperature: int): + if self._target_hotend_temperature != temperature: + self._target_hotend_temperature = temperature + self.targetHotendTemperatureChanged.emit() + + ## Set the target hotend temperature. This ensures that it's actually sent to the remote. + @pyqtSlot(int) + def setTargetHotendTemperature(self, temperature: int): + self._setTargetHotendTemperature(temperature) + self.updateTargetHotendTemperature(temperature) + + @pyqtProperty(int, notify = targetHotendTemperatureChanged) + def targetHotendTemperature(self) -> int: + return self._target_hotend_temperature + + @pyqtProperty(int, notify=hotendTemperatureChanged) + def hotendTemperature(self) -> int: + return self._hotendTemperature + + ## Protected setter for the hotend temperature of the connected printer (if any). + # /parameter temperature Temperature hotend needs to go to (in deg celsius) + # /sa setTargetHotendTemperature + def _setTargetHotendTemperature(self, temperature): + Logger.log("w", "_setTargetHotendTemperature is not implemented by this model") + + @pyqtProperty(str, notify = hotendIDChanged) + def hotendID(self) -> str: + return self._hotend_id + + def updateHotendID(self, id: str): + if self._hotend_id != id: + self._hotend_id = id + self.hotendIDChanged.emit() diff --git a/cura/PrinterOutput/MaterialModel.py b/cura/PrinterOutput/MaterialModel.py new file mode 100644 index 0000000000..41a3680d57 --- /dev/null +++ b/cura/PrinterOutput/MaterialModel.py @@ -0,0 +1,29 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot + + +class MaterialModel(QObject): + def __init__(self, guid, type, color, brand, parent = None): + super().__init__(parent) + self._guid = guid + self._type = type + self._color = color + self._brand = brand + + @pyqtProperty(str, constant = True) + def guid(self): + return self._guid + + @pyqtProperty(str, constant=True) + def type(self): + return self._type + + @pyqtProperty(str, constant=True) + def brand(self): + return self._brand + + @pyqtProperty(str, constant=True) + def color(self): + return self._color \ No newline at end of file diff --git a/cura/PrinterOutput/PrintJobModel.py b/cura/PrinterOutput/PrintJobModel.py new file mode 100644 index 0000000000..9b7952322a --- /dev/null +++ b/cura/PrinterOutput/PrintJobModel.py @@ -0,0 +1,10 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant + + +class PrintJobModel(QObject): + + def __init__(self, parent=None): + super().__init__(parent) \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterModel.py b/cura/PrinterOutput/PrinterModel.py new file mode 100644 index 0000000000..72933ed22a --- /dev/null +++ b/cura/PrinterOutput/PrinterModel.py @@ -0,0 +1,115 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot +from UM.Logger import Logger +from typing import Optional, List + +MYPY = False +if MYPY: + from cura.PrinterOutput.PrintJobModel import PrintJobModel + from cura.PrinterOutput.ExtruderModel import ExtruderModel + + +class PrinterModel(QObject): + bedTemperatureChanged = pyqtSignal() + targetBedTemperatureChanged = pyqtSignal() + printerStateChanged = pyqtSignal() + activePrintJobChanged = pyqtSignal() + nameChanged = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self._bed_temperature = 0 + self._target_bed_temperature = 0 + self._name = "" + + self._extruders = [] # type: List[ExtruderModel] + + self._active_print_job = None # type: Optional[PrintJobModel] + + # Features of the printer; + self._can_pause = True + self._can_abort = True + self._can_pre_heat_bed = True + self._can_control_manually = True + + @pyqtProperty(str, notify=nameChanged) + def name(self): + return self._name + + def setName(self, name): + self._setName(name) + self.updateName(name) + + def _setName(self, name): + Logger.log("w", "_setTargetBedTemperature is not implemented by this model") + + def updateName(self, name): + if self._name != name: + self._name = name + self.nameChanged.emit() + + ## Update the bed temperature. This only changes it locally. + def updateBedTemperature(self, temperature): + if self._bed_temperature != temperature: + self._bed_temperature = temperature + self.bedTemperatureChanged.emit() + + def updateTargetBedTemperature(self, temperature): + if self._target_bed_temperature != temperature: + self._target_bed_temperature = temperature + self.targetBedTemperatureChanged.emit() + + ## Set the target bed temperature. This ensures that it's actually sent to the remote. + @pyqtSlot(int) + def setTargetBedTemperature(self, temperature): + self._setTargetBedTemperature(temperature) + self.updateTargetBedTemperature(temperature) + + ## Protected setter for the bed temperature of the connected printer (if any). + # /parameter temperature Temperature bed needs to go to (in deg celsius) + # /sa setTargetBedTemperature + def _setTargetBedTemperature(self, temperature): + Logger.log("w", "_setTargetBedTemperature is not implemented by this model") + + def updateActivePrintJob(self, print_job): + if self._active_print_job != print_job: + self._active_print_job = print_job + self.activePrintJobChanged.emit() + + @pyqtProperty(QObject, notify = activePrintJobChanged) + def activePrintJob(self): + return self._active_print_job + + @pyqtProperty(str, notify=printerStateChanged) + def printerState(self): + return self._printer_state + + @pyqtProperty(int, notify = bedTemperatureChanged) + def bedTemperature(self): + return self._bed_temperature + + @pyqtProperty(int, notify=targetBedTemperatureChanged) + def targetBedTemperature(self): + return self._target_bed_temperature + + # Does the printer support pre-heating the bed at all + @pyqtProperty(bool, constant=True) + def canPreHeatBed(self): + return self._can_pre_heat_bed + + # Does the printer support pause at all + @pyqtProperty(bool, constant=True) + def canPause(self): + return self._can_pause + + # Does the printer support abort at all + @pyqtProperty(bool, constant=True) + def canAbort(self): + return self._can_abort + + # Does the printer support manual control at all + @pyqtProperty(bool, constant=True) + def canControlManually(self): + return self._can_control_manually diff --git a/cura/PrinterOutput/__init__.py b/cura/PrinterOutput/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 700f7179f1d5cbfed9b88aa56ee41d7c56c07cd8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Nov 2017 16:05:28 +0100 Subject: [PATCH 002/135] Rename models to prevent conflict --- .../{ExtruderModel.py => ExtruderOuputModel.py} | 14 +++++++------- .../{MaterialModel.py => MaterialOutputModel.py} | 2 +- .../{PrintJobModel.py => PrintJobOutputModel.py} | 2 +- .../{PrinterModel.py => PrinterOutputModel.py} | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) rename cura/PrinterOutput/{ExtruderModel.py => ExtruderOuputModel.py} (84%) rename cura/PrinterOutput/{MaterialModel.py => MaterialOutputModel.py} (95%) rename cura/PrinterOutput/{PrintJobModel.py => PrintJobOutputModel.py} (86%) rename cura/PrinterOutput/{PrinterModel.py => PrinterOutputModel.py} (93%) diff --git a/cura/PrinterOutput/ExtruderModel.py b/cura/PrinterOutput/ExtruderOuputModel.py similarity index 84% rename from cura/PrinterOutput/ExtruderModel.py rename to cura/PrinterOutput/ExtruderOuputModel.py index f08b21aaac..d465b7250a 100644 --- a/cura/PrinterOutput/ExtruderModel.py +++ b/cura/PrinterOutput/ExtruderOuputModel.py @@ -8,29 +8,29 @@ from typing import Optional MYPY = False if MYPY: - from cura.PrinterOutput.PrinterModel import PrinterModel - from cura.PrinterOutput.MaterialModel import MaterialModel + from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel -class ExtruderModel(QObject): +class ExtruderOutputModel(QObject): hotendIDChanged = pyqtSignal() targetHotendTemperatureChanged = pyqtSignal() hotendTemperatureChanged = pyqtSignal() activeMaterialChanged = pyqtSignal() - def __init__(self, printer: "PrinterModel", parent=None): + def __init__(self, printer: "PrinterOutputModel", parent=None): super().__init__(parent) self._printer = printer self._target_hotend_temperature = 0 self._hotend_temperature = 0 self._hotend_id = "" - self._active_material = None # type: Optional[MaterialModel] + self._active_material = None # type: Optional[MaterialOutputModel] @pyqtProperty(QObject, notify = activeMaterialChanged) - def activeMaterial(self) -> "MaterialModel": + def activeMaterial(self) -> "MaterialOutputModel": return self._active_material - def updateActiveMaterial(self, material: Optional["MaterialModel"]): + def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]): if self._active_material != material: self._active_material = material self.activeMaterialChanged.emit() diff --git a/cura/PrinterOutput/MaterialModel.py b/cura/PrinterOutput/MaterialOutputModel.py similarity index 95% rename from cura/PrinterOutput/MaterialModel.py rename to cura/PrinterOutput/MaterialOutputModel.py index 41a3680d57..0471b85db8 100644 --- a/cura/PrinterOutput/MaterialModel.py +++ b/cura/PrinterOutput/MaterialOutputModel.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot -class MaterialModel(QObject): +class MaterialOutputModel(QObject): def __init__(self, guid, type, color, brand, parent = None): super().__init__(parent) self._guid = guid diff --git a/cura/PrinterOutput/PrintJobModel.py b/cura/PrinterOutput/PrintJobOutputModel.py similarity index 86% rename from cura/PrinterOutput/PrintJobModel.py rename to cura/PrinterOutput/PrintJobOutputModel.py index 9b7952322a..b2eb3824e3 100644 --- a/cura/PrinterOutput/PrintJobModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant -class PrintJobModel(QObject): +class PrintJobOutputModel(QObject): def __init__(self, parent=None): super().__init__(parent) \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterModel.py b/cura/PrinterOutput/PrinterOutputModel.py similarity index 93% rename from cura/PrinterOutput/PrinterModel.py rename to cura/PrinterOutput/PrinterOutputModel.py index 72933ed22a..ec1a268631 100644 --- a/cura/PrinterOutput/PrinterModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -7,11 +7,11 @@ from typing import Optional, List MYPY = False if MYPY: - from cura.PrinterOutput.PrintJobModel import PrintJobModel - from cura.PrinterOutput.ExtruderModel import ExtruderModel + from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel + from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel -class PrinterModel(QObject): +class PrinterOutputModel(QObject): bedTemperatureChanged = pyqtSignal() targetBedTemperatureChanged = pyqtSignal() printerStateChanged = pyqtSignal() @@ -24,9 +24,9 @@ class PrinterModel(QObject): self._target_bed_temperature = 0 self._name = "" - self._extruders = [] # type: List[ExtruderModel] + self._extruders = [] # type: List[ExtruderOutputModel] - self._active_print_job = None # type: Optional[PrintJobModel] + self._active_print_job = None # type: Optional[PrintJobOutputModel] # Features of the printer; self._can_pause = True From 3a8eef9768721f7d3a95ac89e8a7e67b9da813d9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Nov 2017 16:25:44 +0100 Subject: [PATCH 003/135] Added a printerOutputController to send commands to remote. The idea is that this class can be subclassed. CL-541 --- cura/PrinterOutput/ExtruderOuputModel.py | 8 +--- cura/PrinterOutput/PrintJobOutputModel.py | 44 ++++++++++++++++++- cura/PrinterOutput/PrinterOutputController.py | 21 +++++++++ cura/PrinterOutput/PrinterOutputModel.py | 19 +++----- 4 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 cura/PrinterOutput/PrinterOutputController.py diff --git a/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOuputModel.py index d465b7250a..121e9a69d9 100644 --- a/cura/PrinterOutput/ExtruderOuputModel.py +++ b/cura/PrinterOutput/ExtruderOuputModel.py @@ -49,7 +49,7 @@ class ExtruderOutputModel(QObject): ## Set the target hotend temperature. This ensures that it's actually sent to the remote. @pyqtSlot(int) def setTargetHotendTemperature(self, temperature: int): - self._setTargetHotendTemperature(temperature) + self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self.updateTargetHotendTemperature(temperature) @pyqtProperty(int, notify = targetHotendTemperatureChanged) @@ -60,12 +60,6 @@ class ExtruderOutputModel(QObject): def hotendTemperature(self) -> int: return self._hotendTemperature - ## Protected setter for the hotend temperature of the connected printer (if any). - # /parameter temperature Temperature hotend needs to go to (in deg celsius) - # /sa setTargetHotendTemperature - def _setTargetHotendTemperature(self, temperature): - Logger.log("w", "_setTargetHotendTemperature is not implemented by this model") - @pyqtProperty(str, notify = hotendIDChanged) def hotendID(self) -> str: return self._hotend_id diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index b2eb3824e3..1e0d82f1b0 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -2,9 +2,49 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant +MYPY = False +if MYPY: + from cura.PrinterOutput.PrinterOutputController import PrinterOutputController class PrintJobOutputModel(QObject): + stateChanged = pyqtSignal() + timeTotalChanged = pyqtSignal() + timeElapsedChanged = pyqtSignal() - def __init__(self, parent=None): - super().__init__(parent) \ No newline at end of file + def __init__(self, output_controller: "PrinterOutputController", parent=None): + super().__init__(parent) + self._output_controller = output_controller + self._state = "" + self._time_total = 0 + self._time_elapsed = 0 + + @pyqtProperty(int, notify = timeTotalChanged) + def timeTotal(self): + return self._time_total + + @pyqtProperty(int, notify = timeElapsedChanged) + def timeElapsed(self): + return self._time_elapsed + + @pyqtProperty(str, notify=stateChanged) + def state(self): + return self._state + + def updateTimeTotal(self, new_time_total): + if self._time_total != new_time_total: + self._time_total = new_time_total + self.timeTotalChanged.emit() + + def updateTimeElapsed(self, new_time_elapsed): + if self._time_elapsed != new_time_elapsed: + self._time_elapsed = new_time_elapsed + self.timeElapsedChanged.emit() + + def updateState(self, new_state): + if self._state != new_state: + self._state = new_state + self.stateChanged.emit() + + def setState(self, state): + self._output_controller.setJobState(self, state) \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py new file mode 100644 index 0000000000..c69b49e6e3 --- /dev/null +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -0,0 +1,21 @@ + + +MYPY = False +if MYPY: + from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel + from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel + from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + +class PrinterOutputController: + def __init__(self): + pass + + def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int): + # TODO: implement + pass + + def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): + pass + + def setJobState(self, job: "PrintJobOutputModel", state: str): + pass \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index ec1a268631..d34883a56b 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -9,6 +9,7 @@ MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel + from cura.PrinterOutput.PrinterOutputController import PrinterOutputController class PrinterOutputModel(QObject): @@ -18,12 +19,12 @@ class PrinterOutputModel(QObject): activePrintJobChanged = pyqtSignal() nameChanged = pyqtSignal() - def __init__(self, parent=None): + def __init__(self, output_controller: "PrinterOutputController", parent=None): super().__init__(parent) self._bed_temperature = 0 self._target_bed_temperature = 0 self._name = "" - + self._controller = output_controller self._extruders = [] # type: List[ExtruderOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel] @@ -34,6 +35,9 @@ class PrinterOutputModel(QObject): self._can_pre_heat_bed = True self._can_control_manually = True + def getController(self): + return self._controller + @pyqtProperty(str, notify=nameChanged) def name(self): return self._name @@ -42,9 +46,6 @@ class PrinterOutputModel(QObject): self._setName(name) self.updateName(name) - def _setName(self, name): - Logger.log("w", "_setTargetBedTemperature is not implemented by this model") - def updateName(self, name): if self._name != name: self._name = name @@ -64,15 +65,9 @@ class PrinterOutputModel(QObject): ## Set the target bed temperature. This ensures that it's actually sent to the remote. @pyqtSlot(int) def setTargetBedTemperature(self, temperature): - self._setTargetBedTemperature(temperature) + self._controller.setTargetBedTemperature(self, temperature) self.updateTargetBedTemperature(temperature) - ## Protected setter for the bed temperature of the connected printer (if any). - # /parameter temperature Temperature bed needs to go to (in deg celsius) - # /sa setTargetBedTemperature - def _setTargetBedTemperature(self, temperature): - Logger.log("w", "_setTargetBedTemperature is not implemented by this model") - def updateActivePrintJob(self, print_job): if self._active_print_job != print_job: self._active_print_job = print_job From f0a8db3d4ea3de703ae129d29d7017b3298fcc5e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Nov 2017 16:48:36 +0100 Subject: [PATCH 004/135] Add way to set head position CL-541 --- cura/PrinterOutput/PrinterOutputController.py | 12 ++++ cura/PrinterOutput/PrinterOutputModel.py | 58 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index c69b49e6e3..be077dd352 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -18,4 +18,16 @@ class PrinterOutputController: pass def setJobState(self, job: "PrintJobOutputModel", state: str): + pass + + def cancelPreheatBed(self, printer: "PrinterOutputModel"): + pass + + def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): + pass + + def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed): + pass + + def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): pass \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index d34883a56b..407a433bb4 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -4,6 +4,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot from UM.Logger import Logger from typing import Optional, List +from UM.Math.Vector import Vector MYPY = False if MYPY: @@ -18,15 +19,19 @@ class PrinterOutputModel(QObject): printerStateChanged = pyqtSignal() activePrintJobChanged = pyqtSignal() nameChanged = pyqtSignal() + headPositionChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", parent=None): + def __init__(self, output_controller: "PrinterOutputController", extruders: Optional["ExtruderOutputModel"] = None, parent=None): super().__init__(parent) self._bed_temperature = 0 self._target_bed_temperature = 0 self._name = "" self._controller = output_controller self._extruders = [] # type: List[ExtruderOutputModel] + if self._extruders is not None: + self._extruders = extruders + self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] # Features of the printer; @@ -35,6 +40,57 @@ class PrinterOutputModel(QObject): self._can_pre_heat_bed = True self._can_control_manually = True + @pyqtProperty("QVariantList", constant = True) + def extruders(self): + return self._extruders + + @pyqtProperty(QVariant, notify = headPositionChanged) + def headPosition(self): + return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position_z} + + def updateHeadPosition(self, x, y, z): + if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z: + self._head_position = Vector(x, y, z) + self.headPositionChanged.emit() + + @pyqtProperty("long", "long", "long") + @pyqtProperty("long", "long", "long", "long") + def setHeadPosition(self, x, y, z, speed = 3000): + self._controller.setHeadPosition(self, x, y, z, speed) + + @pyqtProperty("long") + @pyqtProperty("long", "long") + def setHeadX(self, x, speed = 3000): + self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed) + + @pyqtProperty("long") + @pyqtProperty("long", "long") + def setHeadY(self, y, speed = 3000): + self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed) + + @pyqtProperty("long") + @pyqtProperty("long", "long") + def setHeadY(self, z, speed = 3000): + self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed) + + @pyqtSlot("long", "long", "long") + @pyqtSlot("long", "long", "long", "long") + def moveHead(self, x = 0, y = 0, z = 0, speed = 3000): + self._controller.moveHead(self, x, y, z, speed) + + ## Pre-heats the heated bed of the printer. + # + # \param temperature The temperature to heat the bed to, in degrees + # Celsius. + # \param duration How long the bed should stay warm, in seconds. + @pyqtSlot(float, float) + def preheatBed(self, temperature, duration): + self._controller.preheatBed(self, temperature, duration) + + @pyqtSlot() + def cancelPreheatBed(self): + self._controller.cancelPreheatBed(self) + def getController(self): return self._controller From 00a5127b192325c99d5f2e5e23850cebade54e8c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Nov 2017 17:00:09 +0100 Subject: [PATCH 005/135] Added home head & bed CL-541 --- cura/PrinterOutput/PrinterOutputController.py | 6 ++++++ cura/PrinterOutput/PrinterOutputModel.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index be077dd352..0625a8ef9f 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -30,4 +30,10 @@ class PrinterOutputController: pass def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): + pass + + def homeBed(self, printer): + pass + + def homeHead(self, printer): pass \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 407a433bb4..ab8ca83ec6 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -40,6 +40,14 @@ class PrinterOutputModel(QObject): self._can_pre_heat_bed = True self._can_control_manually = True + @pyqtSlot() + def homeHead(self): + self._controller.homeHead(self) + + @pyqtSlot() + def homeBed(self): + self._controller.homeBed(self) + @pyqtProperty("QVariantList", constant = True) def extruders(self): return self._extruders From b63880e57f34e2ebeea1de292fac845263adf91e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 17 Nov 2017 17:00:32 +0100 Subject: [PATCH 006/135] Printer Output model now must have at least one extruder CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index ab8ca83ec6..00644980b4 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -21,15 +21,13 @@ class PrinterOutputModel(QObject): nameChanged = pyqtSignal() headPositionChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", extruders: Optional["ExtruderOutputModel"] = None, parent=None): + def __init__(self, output_controller: "PrinterOutputController", extruders: List["ExtruderOutputModel"], parent=None): super().__init__(parent) self._bed_temperature = 0 self._target_bed_temperature = 0 self._name = "" self._controller = output_controller - self._extruders = [] # type: List[ExtruderOutputModel] - if self._extruders is not None: - self._extruders = extruders + self._extruders = extruders self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] From 22f2279a768fa79c40e51418b1cf2fde2bd1586f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 10:59:57 +0100 Subject: [PATCH 007/135] Moved bunch of code from the old location to the new bits in archtiecture CL-541 --- .../NetworkedPrinterOutputDevice.py | 71 ++ cura/PrinterOutput/PrinterOutputController.py | 1 + cura/PrinterOutputDevice.py | 620 +----------------- .../UM3PrinterOutputDevicePlugin.py | 2 + 4 files changed, 92 insertions(+), 602 deletions(-) create mode 100644 cura/PrinterOutput/NetworkedPrinterOutputDevice.py create mode 100644 plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py new file mode 100644 index 0000000000..dc02fa839d --- /dev/null +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -0,0 +1,71 @@ +from UM.Application import Application +from cura.PrinterOutputDevice import PrinterOutputDevice + +from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply +from PyQt5.QtCore import QUrl + +from time import time +from typing import Callable + +class NetworkedPrinterOutputDevice(PrinterOutputDevice): + def __init__(self, device_id, parent = None): + super().__init__(device_id = device_id, parent = parent) + self._manager = None + self._createNetworkManager() + self._last_response_time = time() + self._last_request_time = None + self._api_prefix = "" + self._address = "" + + self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()) + + self._onFinishedCallbacks = {} + + def _update(self): + if not self._manager.networkAccessible(): + pass # TODO: no internet connection. + + pass + + def _createEmptyRequest(self, target): + url = QUrl("http://" + self._address + self._api_prefix + target) + request = QNetworkRequest(url) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) + return request + + def _put(self, target: str, data: str, onFinished: Callable): + request = self._createEmptyRequest(target) + self._onFinishedCallbacks[request] = onFinished + self._manager.put(request, data.encode()) + + def _get(self, target: str, onFinished: Callable): + request = self._createEmptyRequest(target) + self._onFinishedCallbacks[request] = onFinished + self._manager.get(request) + + def _delete(self, target: str, onFinished: Callable): + pass + + def _post(self, target: str, data: str, onFinished: Callable, onProgress: Callable): + pass + + def _createNetworkManager(self): + if self._manager: + self._manager.finished.disconnect(self.__handleOnFinished) + #self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) + #self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) + + self._manager = QNetworkAccessManager() + self._manager.finished.connect(self.__handleOnFinished) + #self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + #self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes + + def __handleOnFinished(self, reply: QNetworkReply): + self._last_response_time = time() + try: + self._onFinishedCallbacks[reply.request()](reply) + del self._onFinishedCallbacks[reply.request] # Remove the callback. + except Exception as e: + print("Something went wrong with callback", e) + pass \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 0625a8ef9f..9f9a26a2a5 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -6,6 +6,7 @@ if MYPY: from cura.PrinterOutput.ExtruderOuputModel import ExtruderOuputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + class PrinterOutputController: def __init__(self): pass diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 837ecc97c6..3f12c2f40c 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -5,13 +5,10 @@ from UM.i18n import i18nCatalog from UM.OutputDevice.OutputDevice import OutputDevice from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl from PyQt5.QtQml import QQmlComponent, QQmlContext -from PyQt5.QtWidgets import QMessageBox from enum import IntEnum # For the connection state tracking. -from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Logger import Logger from UM.Signal import signalemitter -from UM.PluginRegistry import PluginRegistry from UM.Application import Application import os @@ -29,38 +26,12 @@ i18n_catalog = i18nCatalog("cura") # For all other uses it should be used in the same way as a "regular" OutputDevice. @signalemitter class PrinterOutputDevice(QObject, OutputDevice): + printersChanged = pyqtSignal + def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) - self._container_registry = ContainerRegistry.getInstance() - self._target_bed_temperature = 0 - self._bed_temperature = 0 - self._num_extruders = 1 - self._hotend_temperatures = [0] * self._num_extruders - self._target_hotend_temperatures = [0] * self._num_extruders - self._material_ids = [""] * self._num_extruders - self._hotend_ids = [""] * self._num_extruders - self._progress = 0 - self._head_x = 0 - self._head_y = 0 - self._head_z = 0 - self._connection_state = ConnectionState.closed - self._connection_text = "" - self._time_elapsed = 0 - self._time_total = 0 - self._job_state = "" - self._job_name = "" - self._error_text = "" - self._accepts_commands = True - self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds. - self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still. - self._preheat_bed_timer.setSingleShot(True) - self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed) - - self._printer_state = "" - self._printer_type = "unknown" - - self._camera_active = False + self._printers = [] self._monitor_view_qml_path = "" self._monitor_component = None @@ -71,84 +42,24 @@ class PrinterOutputDevice(QObject, OutputDevice): self._control_item = None self._qml_context = None - self._can_pause = True - self._can_abort = True - self._can_pre_heat_bed = True - self._can_control_manually = True + + self._update_timer = QTimer() + self._update_timer.setInterval(2000) # TODO; Add preference for update interval + self._update_timer.setSingleShot(False) + self._update_timer.timeout.connect(self._update) + + def _update(self): + pass def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): raise NotImplementedError("requestWrite needs to be implemented") - ## Signals + @pyqtProperty(QObject, notify = printersChanged) + def activePrinter(self): + if len(self._printers): - # Signal to be emitted when bed temp is changed - bedTemperatureChanged = pyqtSignal() - - # Signal to be emitted when target bed temp is changed - targetBedTemperatureChanged = pyqtSignal() - - # Signal when the progress is changed (usually when this output device is printing / sending lots of data) - progressChanged = pyqtSignal() - - # Signal to be emitted when hotend temp is changed - hotendTemperaturesChanged = pyqtSignal() - - # Signal to be emitted when target hotend temp is changed - targetHotendTemperaturesChanged = pyqtSignal() - - # Signal to be emitted when head position is changed (x,y,z) - headPositionChanged = pyqtSignal() - - # Signal to be emitted when either of the material ids is changed - materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"]) - - # Signal to be emitted when either of the hotend ids is changed - hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"]) - - # Signal that is emitted every time connection state is changed. - # it also sends it's own device_id (for convenience sake) - connectionStateChanged = pyqtSignal(str) - - connectionTextChanged = pyqtSignal() - - timeElapsedChanged = pyqtSignal() - - timeTotalChanged = pyqtSignal() - - jobStateChanged = pyqtSignal() - - jobNameChanged = pyqtSignal() - - errorTextChanged = pyqtSignal() - - acceptsCommandsChanged = pyqtSignal() - - printerStateChanged = pyqtSignal() - - printerTypeChanged = pyqtSignal() - - # Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally). - preheatBedRemainingTimeChanged = pyqtSignal() - - # Does the printer support pre-heating the bed at all - @pyqtProperty(bool, constant=True) - def canPreHeatBed(self): - return self._can_pre_heat_bed - - # Does the printer support pause at all - @pyqtProperty(bool, constant=True) - def canPause(self): - return self._can_pause - - # Does the printer support abort at all - @pyqtProperty(bool, constant=True) - def canAbort(self): - return self._can_abort - - # Does the printer support manual control at all - @pyqtProperty(bool, constant=True) - def canControlManually(self): - return self._can_control_manually + return self._printers[0] + return None @pyqtProperty(QObject, constant=True) def monitorItem(self): @@ -204,513 +115,18 @@ class PrinterOutputDevice(QObject, OutputDevice): Logger.log("e", "QQmlComponent status %s", self._monitor_component.status()) Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString()) - @pyqtProperty(str, notify=printerTypeChanged) - def printerType(self): - return self._printer_type - - @pyqtProperty(str, notify=printerStateChanged) - def printerState(self): - return self._printer_state - - @pyqtProperty(str, notify = jobStateChanged) - def jobState(self): - return self._job_state - - def _updatePrinterType(self, printer_type): - if self._printer_type != printer_type: - self._printer_type = printer_type - self.printerTypeChanged.emit() - - def _updatePrinterState(self, printer_state): - if self._printer_state != printer_state: - self._printer_state = printer_state - self.printerStateChanged.emit() - - def _updateJobState(self, job_state): - if self._job_state != job_state: - self._job_state = job_state - self.jobStateChanged.emit() - - @pyqtSlot(str) - def setJobState(self, job_state): - self._setJobState(job_state) - - def _setJobState(self, job_state): - Logger.log("w", "_setJobState is not implemented by this output device") - - @pyqtSlot() - def startCamera(self): - self._camera_active = True - self._startCamera() - - def _startCamera(self): - Logger.log("w", "_startCamera is not implemented by this output device") - - @pyqtSlot() - def stopCamera(self): - self._camera_active = False - self._stopCamera() - - def _stopCamera(self): - Logger.log("w", "_stopCamera is not implemented by this output device") - - @pyqtProperty(str, notify = jobNameChanged) - def jobName(self): - return self._job_name - - def setJobName(self, name): - if self._job_name != name: - self._job_name = name - self.jobNameChanged.emit() - - ## Gives a human-readable address where the device can be found. - @pyqtProperty(str, constant = True) - def address(self): - Logger.log("w", "address is not implemented by this output device.") - - ## A human-readable name for the device. - @pyqtProperty(str, constant = True) - def name(self): - Logger.log("w", "name is not implemented by this output device.") - return "" - - @pyqtProperty(str, notify = errorTextChanged) - def errorText(self): - return self._error_text - - ## Set the error-text that is shown in the print monitor in case of an error - def setErrorText(self, error_text): - if self._error_text != error_text: - self._error_text = error_text - self.errorTextChanged.emit() - - @pyqtProperty(bool, notify = acceptsCommandsChanged) - def acceptsCommands(self): - return self._accepts_commands - - ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands - def setAcceptsCommands(self, accepts_commands): - if self._accepts_commands != accepts_commands: - self._accepts_commands = accepts_commands - self.acceptsCommandsChanged.emit() - - ## Get the bed temperature of the bed (if any) - # This function is "final" (do not re-implement) - # /sa _getBedTemperature implementation function - @pyqtProperty(float, notify = bedTemperatureChanged) - def bedTemperature(self): - return self._bed_temperature - - ## Set the (target) bed temperature - # This function is "final" (do not re-implement) - # /param temperature new target temperature of the bed (in deg C) - # /sa _setTargetBedTemperature implementation function - @pyqtSlot(int) - def setTargetBedTemperature(self, temperature): - self._setTargetBedTemperature(temperature) - if self._target_bed_temperature != temperature: - self._target_bed_temperature = temperature - self.targetBedTemperatureChanged.emit() - - ## The total duration of the time-out to pre-heat the bed, in seconds. - # - # \return The duration of the time-out to pre-heat the bed, in seconds. - @pyqtProperty(int, constant = True) - def preheatBedTimeout(self): - return self._preheat_bed_timeout - - ## The remaining duration of the pre-heating of the bed. - # - # This is formatted in M:SS format. - # \return The duration of the time-out to pre-heat the bed, formatted. - @pyqtProperty(str, notify = preheatBedRemainingTimeChanged) - def preheatBedRemainingTime(self): - if not self._preheat_bed_timer.isActive(): - return "" - period = self._preheat_bed_timer.remainingTime() - if period <= 0: - return "" - minutes, period = divmod(period, 60000) #60000 milliseconds in a minute. - seconds, _ = divmod(period, 1000) #1000 milliseconds in a second. - if minutes <= 0 and seconds <= 0: - return "" - return "%d:%02d" % (minutes, seconds) - - ## Time the print has been printing. - # Note that timeTotal - timeElapsed should give time remaining. - @pyqtProperty(float, notify = timeElapsedChanged) - def timeElapsed(self): - return self._time_elapsed - - ## Total time of the print - # Note that timeTotal - timeElapsed should give time remaining. - @pyqtProperty(float, notify=timeTotalChanged) - def timeTotal(self): - return self._time_total - - @pyqtSlot(float) - def setTimeTotal(self, new_total): - if self._time_total != new_total: - self._time_total = new_total - self.timeTotalChanged.emit() - - @pyqtSlot(float) - def setTimeElapsed(self, time_elapsed): - if self._time_elapsed != time_elapsed: - self._time_elapsed = time_elapsed - self.timeElapsedChanged.emit() - - ## Home the head of the connected printer - # This function is "final" (do not re-implement) - # /sa _homeHead implementation function - @pyqtSlot() - def homeHead(self): - self._homeHead() - - ## Home the head of the connected printer - # This is an implementation function and should be overriden by children. - def _homeHead(self): - Logger.log("w", "_homeHead is not implemented by this output device") - - ## Home the bed of the connected printer - # This function is "final" (do not re-implement) - # /sa _homeBed implementation function - @pyqtSlot() - def homeBed(self): - self._homeBed() - - ## Home the bed of the connected printer - # This is an implementation function and should be overriden by children. - # /sa homeBed - def _homeBed(self): - Logger.log("w", "_homeBed is not implemented by this output device") - - ## Protected setter for the bed temperature of the connected printer (if any). - # /parameter temperature Temperature bed needs to go to (in deg celsius) - # /sa setTargetBedTemperature - def _setTargetBedTemperature(self, temperature): - Logger.log("w", "_setTargetBedTemperature is not implemented by this output device") - - ## Pre-heats the heated bed of the printer. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. - @pyqtSlot(float, float) - def preheatBed(self, temperature, duration): - Logger.log("w", "preheatBed is not implemented by this output device.") - - ## Cancels pre-heating the heated bed of the printer. - # - # If the bed is not pre-heated, nothing happens. - @pyqtSlot() - def cancelPreheatBed(self): - Logger.log("w", "cancelPreheatBed is not implemented by this output device.") - - ## Protected setter for the current bed temperature. - # This simply sets the bed temperature, but ensures that a signal is emitted. - # /param temperature temperature of the bed. - def _setBedTemperature(self, temperature): - if self._bed_temperature != temperature: - self._bed_temperature = temperature - self.bedTemperatureChanged.emit() - - ## Get the target bed temperature if connected printer (if any) - @pyqtProperty(int, notify = targetBedTemperatureChanged) - def targetBedTemperature(self): - return self._target_bed_temperature - - ## Set the (target) hotend temperature - # This function is "final" (do not re-implement) - # /param index the index of the hotend that needs to change temperature - # /param temperature The temperature it needs to change to (in deg celsius). - # /sa _setTargetHotendTemperature implementation function - @pyqtSlot(int, int) - def setTargetHotendTemperature(self, index, temperature): - self._setTargetHotendTemperature(index, temperature) - - if self._target_hotend_temperatures[index] != temperature: - self._target_hotend_temperatures[index] = temperature - self.targetHotendTemperaturesChanged.emit() - - ## Implementation function of setTargetHotendTemperature. - # /param index Index of the hotend to set the temperature of - # /param temperature Temperature to set the hotend to (in deg C) - # /sa setTargetHotendTemperature - def _setTargetHotendTemperature(self, index, temperature): - Logger.log("w", "_setTargetHotendTemperature is not implemented by this output device") - - @pyqtProperty("QVariantList", notify = targetHotendTemperaturesChanged) - def targetHotendTemperatures(self): - return self._target_hotend_temperatures - - @pyqtProperty("QVariantList", notify = hotendTemperaturesChanged) - def hotendTemperatures(self): - return self._hotend_temperatures - - ## Protected setter for the current hotend temperature. - # This simply sets the hotend temperature, but ensures that a signal is emitted. - # /param index Index of the hotend - # /param temperature temperature of the hotend (in deg C) - def _setHotendTemperature(self, index, temperature): - if self._hotend_temperatures[index] != temperature: - self._hotend_temperatures[index] = temperature - self.hotendTemperaturesChanged.emit() - - @pyqtProperty("QVariantList", notify = materialIdChanged) - def materialIds(self): - return self._material_ids - - @pyqtProperty("QVariantList", notify = materialIdChanged) - def materialNames(self): - result = [] - for material_id in self._material_ids: - if material_id is None: - result.append(i18n_catalog.i18nc("@item:material", "No material loaded")) - continue - - containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id) - if containers: - result.append(containers[0].getName()) - else: - result.append(i18n_catalog.i18nc("@item:material", "Unknown material")) - return result - - ## List of the colours of the currently loaded materials. - # - # The list is in order of extruders. If there is no material in an - # extruder, the colour is shown as transparent. - # - # The colours are returned in hex-format AARRGGBB or RRGGBB - # (e.g. #800000ff for transparent blue or #00ff00 for pure green). - @pyqtProperty("QVariantList", notify = materialIdChanged) - def materialColors(self): - result = [] - for material_id in self._material_ids: - if material_id is None: - result.append("#00000000") #No material. - continue - - containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id) - if containers: - result.append(containers[0].getMetaDataEntry("color_code")) - else: - result.append("#00000000") #Unknown material. - return result - - ## Protected setter for the current material id. - # /param index Index of the extruder - # /param material_id id of the material - def _setMaterialId(self, index, material_id): - if material_id and material_id != "" and material_id != self._material_ids[index]: - Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id)) - self._material_ids[index] = material_id - self.materialIdChanged.emit(index, material_id) - - @pyqtProperty("QVariantList", notify = hotendIdChanged) - def hotendIds(self): - return self._hotend_ids - - ## Protected setter for the current hotend id. - # /param index Index of the extruder - # /param hotend_id id of the hotend - def _setHotendId(self, index, hotend_id): - if hotend_id and hotend_id != self._hotend_ids[index]: - Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id)) - self._hotend_ids[index] = hotend_id - self.hotendIdChanged.emit(index, hotend_id) - elif not hotend_id: - Logger.log("d", "Removing hotend id of hotend %d.", index) - self._hotend_ids[index] = None - self.hotendIdChanged.emit(index, None) - - ## Let the user decide if the hotends and/or material should be synced with the printer - # NB: the UX needs to be implemented by the plugin - def materialHotendChangedMessage(self, callback): - Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'") - callback(QMessageBox.Yes) - ## Attempt to establish connection def connect(self): - raise NotImplementedError("connect needs to be implemented") + self._update_timer.start() ## Attempt to close the connection def close(self): - raise NotImplementedError("close needs to be implemented") - - @pyqtProperty(bool, notify = connectionStateChanged) - def connectionState(self): - return self._connection_state - - ## Set the connection state of this output device. - # /param connection_state ConnectionState enum. - def setConnectionState(self, connection_state): - if self._connection_state != connection_state: - self._connection_state = connection_state - self.connectionStateChanged.emit(self._id) - - @pyqtProperty(str, notify = connectionTextChanged) - def connectionText(self): - return self._connection_text - - ## Set a text that is shown on top of the print monitor tab - def setConnectionText(self, connection_text): - if self._connection_text != connection_text: - self._connection_text = connection_text - self.connectionTextChanged.emit() + self._update_timer.stop() ## Ensure that close gets called when object is destroyed def __del__(self): self.close() - ## Get the x position of the head. - # This function is "final" (do not re-implement) - @pyqtProperty(float, notify = headPositionChanged) - def headX(self): - return self._head_x - - ## Get the y position of the head. - # This function is "final" (do not re-implement) - @pyqtProperty(float, notify = headPositionChanged) - def headY(self): - return self._head_y - - ## Get the z position of the head. - # In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements. - # This function is "final" (do not re-implement) - @pyqtProperty(float, notify = headPositionChanged) - def headZ(self): - return self._head_z - - ## Update the saved position of the head - # This function should be called when a new position for the head is received. - def _updateHeadPosition(self, x, y ,z): - position_changed = False - if self._head_x != x: - self._head_x = x - position_changed = True - if self._head_y != y: - self._head_y = y - position_changed = True - if self._head_z != z: - self._head_z = z - position_changed = True - - if position_changed: - self.headPositionChanged.emit() - - ## Set the position of the head. - # In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements. - # This function is "final" (do not re-implement) - # /param x new x location of the head. - # /param y new y location of the head. - # /param z new z location of the head. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _setHeadPosition implementation function - @pyqtSlot("long", "long", "long") - @pyqtSlot("long", "long", "long", "long") - def setHeadPosition(self, x, y, z, speed = 3000): - self._setHeadPosition(x, y , z, speed) - - ## Set the X position of the head. - # This function is "final" (do not re-implement) - # /param x x position head needs to move to. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _setHeadx implementation function - @pyqtSlot("long") - @pyqtSlot("long", "long") - def setHeadX(self, x, speed = 3000): - self._setHeadX(x, speed) - - ## Set the Y position of the head. - # This function is "final" (do not re-implement) - # /param y y position head needs to move to. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _setHeadY implementation function - @pyqtSlot("long") - @pyqtSlot("long", "long") - def setHeadY(self, y, speed = 3000): - self._setHeadY(y, speed) - - ## Set the Z position of the head. - # In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements. - # This function is "final" (do not re-implement) - # /param z z position head needs to move to. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _setHeadZ implementation function - @pyqtSlot("long") - @pyqtSlot("long", "long") - def setHeadZ(self, z, speed = 3000): - self._setHeadZ(z, speed) - - ## Move the head of the printer. - # Note that this is a relative move. If you want to move the head to a specific position you can use - # setHeadPosition - # This function is "final" (do not re-implement) - # /param x distance in x to move - # /param y distance in y to move - # /param z distance in z to move - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _moveHead implementation function - @pyqtSlot("long", "long", "long") - @pyqtSlot("long", "long", "long", "long") - def moveHead(self, x = 0, y = 0, z = 0, speed = 3000): - self._moveHead(x, y, z, speed) - - ## Implementation function of moveHead. - # /param x distance in x to move - # /param y distance in y to move - # /param z distance in z to move - # /param speed Speed by which it needs to move (in mm/minute) - # /sa moveHead - def _moveHead(self, x, y, z, speed): - Logger.log("w", "_moveHead is not implemented by this output device") - - ## Implementation function of setHeadPosition. - # /param x new x location of the head. - # /param y new y location of the head. - # /param z new z location of the head. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa setHeadPosition - def _setHeadPosition(self, x, y, z, speed): - Logger.log("w", "_setHeadPosition is not implemented by this output device") - - ## Implementation function of setHeadX. - # /param x new x location of the head. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa setHeadX - def _setHeadX(self, x, speed): - Logger.log("w", "_setHeadX is not implemented by this output device") - - ## Implementation function of setHeadY. - # /param y new y location of the head. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _setHeadY - def _setHeadY(self, y, speed): - Logger.log("w", "_setHeadY is not implemented by this output device") - - ## Implementation function of setHeadZ. - # /param z new z location of the head. - # /param speed Speed by which it needs to move (in mm/minute) - # /sa _setHeadZ - def _setHeadZ(self, z, speed): - Logger.log("w", "_setHeadZ is not implemented by this output device") - - ## Get the progress of any currently active process. - # This function is "final" (do not re-implement) - # /sa _getProgress - # /returns float progress of the process. -1 indicates that there is no process. - @pyqtProperty(float, notify = progressChanged) - def progress(self): - return self._progress - - ## Set the progress of any currently active process - # /param progress Progress of the process. - def setProgress(self, progress): - if self._progress != progress: - self._progress = progress - self.progressChanged.emit() - ## The current processing state of the backend. class ConnectionState(IntEnum): diff --git a/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py new file mode 100644 index 0000000000..828fe76b64 --- /dev/null +++ b/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py @@ -0,0 +1,2 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. \ No newline at end of file From c1dbdc64eec11c8e8119bb388515a6bfd7870f9d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 11:34:24 +0100 Subject: [PATCH 008/135] Added missing () CL-541 --- cura/PrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 3f12c2f40c..9db0a26e55 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -26,7 +26,7 @@ i18n_catalog = i18nCatalog("cura") # For all other uses it should be used in the same way as a "regular" OutputDevice. @signalemitter class PrinterOutputDevice(QObject, OutputDevice): - printersChanged = pyqtSignal + printersChanged = pyqtSignal() def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) From 9202bb11fe11a873f44b264e2674723b89bcef1d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 13:12:15 +0100 Subject: [PATCH 009/135] Added stubs for cluster & legacy output devices CL-541 --- .../ClusterUM3OutputDevice.py | 5 + .../UM3NetworkPrinting/DiscoverUM3Action.py | 8 +- .../LegacyUM3OutputDevice.py | 5 + .../UM3OutputDevicePlugin.py | 185 ++++++++++++++++++ .../UM3PrinterOutputDevicePlugin.py | 2 - plugins/UM3NetworkPrinting/__init__.py | 4 +- 6 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py create mode 100644 plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py create mode 100644 plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py delete mode 100644 plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py new file mode 100644 index 0000000000..4609e86f20 --- /dev/null +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -0,0 +1,5 @@ +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + +class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): + def __init__(self, device_id, address, properties, parent = None): + super().__init__(device_id = device_id, parent = parent) \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index af1a556892..f199f7cd24 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -37,7 +37,7 @@ class DiscoverUM3Action(MachineAction): if not self._network_plugin: Logger.log("d", "Starting printer discovery.") self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") - self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged) + self._network_plugin.discoveredDevicesChanged.connect(self._onPrinterDiscoveryChanged) self.printersChanged.emit() ## Re-filters the list of printers. @@ -87,10 +87,10 @@ class DiscoverUM3Action(MachineAction): 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.sort(key = lambda k: k.name) + #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: return [] diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py new file mode 100644 index 0000000000..0e19df4c18 --- /dev/null +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -0,0 +1,5 @@ +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + +class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): + def __init__(self, device_id, address, properties, parent = None): + super().__init__(device_id = device_id, parent = parent) \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py new file mode 100644 index 0000000000..37425bfef2 --- /dev/null +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -0,0 +1,185 @@ +# 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 zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo +from queue import Queue +from threading import Event, Thread + +from time import time + +from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice + +## 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) + + self._discovered_devices = {} + + # 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]) + + def stop(self): + if self._zero_conf is not None: + Logger.log("d", "zeroconf close...") + self._zero_conf.close() + + def _onRemoveDevice(self, name): + device = self._discovered_devices.pop(name, None) + if device: + if device.isConnected(): + device.disconnect() + device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + + self.discoveredDevicesChanged.emit() + '''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()''' + + 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() + + pass + ''' + 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()''' + + ## 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 \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py deleted file mode 100644 index 828fe76b64..0000000000 --- a/plugins/UM3NetworkPrinting/UM3PrinterOutputDevicePlugin.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index 37f863bd00..6dd86a16d2 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -5,8 +5,10 @@ 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()} \ No newline at end of file + return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()} \ No newline at end of file From 68e80a88bcbd072ed2e1be743b1135f64bf8160b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 13:18:08 +0100 Subject: [PATCH 010/135] Rename usage of printer to more generic device. The usage of "printer" is a bit confusing, as in the case of CuraConnect it's a device that can acces multiple printers. CL-541 --- .../UM3NetworkPrinting/DiscoverUM3Action.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index f199f7cd24..f7afe3e00f 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -27,24 +27,26 @@ 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 + + discoveredDevicesChanged = pyqtSignal() @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.discoveredDevicesChanged.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): @@ -53,35 +55,36 @@ 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) - 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: From 1b8caa7a21ec0de6fd8f329796c37c2ae748758e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 13:23:09 +0100 Subject: [PATCH 011/135] NetworkedPrinterOutputDevice now requires address in constructor CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 4 ++-- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 5 ++++- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index dc02fa839d..416efe10a3 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -8,14 +8,14 @@ from time import time from typing import Callable class NetworkedPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, device_id, parent = None): + def __init__(self, device_id, address: str, parent = None): super().__init__(device_id = device_id, parent = parent) self._manager = None self._createNetworkManager() self._last_response_time = time() self._last_request_time = None self._api_prefix = "" - self._address = "" + self._address = address self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 4609e86f20..4a89e35275 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -2,4 +2,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address, properties, parent = None): - super().__init__(device_id = device_id, parent = parent) \ No newline at end of file + super().__init__(device_id = device_id, address = address, parent = parent) + + def _update(self): + pass diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 0e19df4c18..ee8501a070 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -1,5 +1,8 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): - def __init__(self, device_id, address, properties, parent = None): - super().__init__(device_id = device_id, parent = parent) \ No newline at end of file + def __init__(self, device_id, address: str, properties, parent = None): + super().__init__(device_id = device_id, address = address, parent = parent) + + def _update(self): + pass From 4197f18fc15a2b9f48c4ad9142d709db4174da45 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 13:35:08 +0100 Subject: [PATCH 012/135] First steps to ensure that the Discover UM3 action works with new architecture CL-541 --- .../NetworkedPrinterOutputDevice.py | 43 +++++++++++++++++-- .../ClusterUM3OutputDevice.py | 5 ++- .../UM3NetworkPrinting/DiscoverUM3Action.qml | 42 +++++++++--------- .../LegacyUM3OutputDevice.py | 2 +- 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 416efe10a3..67ad968ce8 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -2,13 +2,14 @@ from UM.Application import Application from cura.PrinterOutputDevice import PrinterOutputDevice from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl from time import time from typing import Callable + class NetworkedPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, device_id, address: str, parent = None): + def __init__(self, device_id, address: str, properties, parent = None): super().__init__(device_id = device_id, parent = parent) self._manager = None self._createNetworkManager() @@ -16,7 +17,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_request_time = None self._api_prefix = "" self._address = address - + self._properties = properties self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()) self._onFinishedCallbacks = {} @@ -68,4 +69,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): del self._onFinishedCallbacks[reply.request] # Remove the callback. except Exception as e: print("Something went wrong with callback", e) - pass \ No newline at end of file + pass + + @pyqtSlot(str, result=str) + def getProperty(self, key): + key = key.encode("utf-8") + if key in self._properties: + return self._properties.get(key, b"").decode("utf-8") + else: + return "" + + ## Get the unique key of this machine + # \return key String containing the key of the machine. + @pyqtProperty(str, constant=True) + def key(self): + return self._id + + ## The IP address of the printer. + @pyqtProperty(str, constant=True) + def address(self): + return self._properties.get(b"address", b"").decode("utf-8") + + ## Name of the printer (as returned from the ZeroConf properties) + @pyqtProperty(str, constant=True) + def name(self): + return self._properties.get(b"name", b"").decode("utf-8") + + ## Firmware version (as returned from the ZeroConf properties) + @pyqtProperty(str, constant=True) + def firmwareVersion(self): + return self._properties.get(b"firmware_version", b"").decode("utf-8") + + ## IPadress of this printer + @pyqtProperty(str, constant=True) + def ipAddress(self): + return self._address \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 4a89e35275..f4e60b49e4 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -1,8 +1,11 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address, properties, parent = None): - super().__init__(device_id = device_id, address = address, parent = parent) + super().__init__(device_id = device_id, address = address, properties=properties, parent = parent) def _update(self): + super()._update() + pass diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index cec2bf0f0f..8131493957 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -10,7 +10,7 @@ Cura.MachineAction { id: base anchors.fill: parent; - property var selectedPrinter: null + property var selectedDevice: null property bool completeProperties: true Connections @@ -31,7 +31,7 @@ Cura.MachineAction { if(base.selectedPrinter && 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.removeManualPrinter(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,12 +226,12 @@ 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", "Ultimaker 3") - } else if(base.selectedPrinter.printerType == "ultimaker3_extended") + } else if(base.selectedDevice.printerType == "ultimaker3_extended") { return catalog.i18nc("@label", "Ultimaker 3 Extended") } 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() } } diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index ee8501a070..c7ccbe763a 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -2,7 +2,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent = None): - super().__init__(device_id = device_id, address = address, parent = parent) + super().__init__(device_id = device_id, address = address, properties = properties, parent = parent) def _update(self): pass From 59e4d1af6306d9d0751fb84d7cf7e19b64fc3131 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 15:11:38 +0100 Subject: [PATCH 013/135] re-added recheck connections CL-541 --- .../UM3OutputDevicePlugin.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 37425bfef2..0d1154e07c 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -60,6 +60,28 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest]) + 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._printers[key].close() + self._printers[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + + def _onDeviceConnectionStateChanged(self, key): + pass # TODO + def stop(self): if self._zero_conf is not None: Logger.log("d", "zeroconf close...") From 03304003af90c2d3566eb3e5cc504ca8e22ff96f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 15:12:11 +0100 Subject: [PATCH 014/135] Added connection state property Cl-541 --- cura/PrinterOutputDevice.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 9db0a26e55..573fe63158 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -27,6 +27,7 @@ i18n_catalog = i18nCatalog("cura") @signalemitter class PrinterOutputDevice(QObject, OutputDevice): printersChanged = pyqtSignal() + connectionStateChanged = pyqtSignal() def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) @@ -48,6 +49,11 @@ class PrinterOutputDevice(QObject, OutputDevice): self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) + self._connection_state = ConnectionState.closed + + def isConnected(self): + return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error + def _update(self): pass From 61753540e405d1b40e27e304c60876242f5749f3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 15:12:29 +0100 Subject: [PATCH 015/135] Callbacks are now handled by url and operation type. It would have been nicer to use the request, but it's unhashable. Cl-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 11 +++++------ plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 -- plugins/UM3NetworkPrinting/DiscoverUM3Action.qml | 6 ++++++ plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 3 ++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 67ad968ce8..e33834ffce 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -37,13 +37,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _put(self, target: str, data: str, onFinished: Callable): request = self._createEmptyRequest(target) - self._onFinishedCallbacks[request] = onFinished - self._manager.put(request, data.encode()) + reply = self._manager.put(request, data.encode()) + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished def _get(self, target: str, onFinished: Callable): request = self._createEmptyRequest(target) - self._onFinishedCallbacks[request] = onFinished - self._manager.get(request) + reply = self._manager.get(request) + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished def _delete(self, target: str, onFinished: Callable): pass @@ -65,8 +65,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def __handleOnFinished(self, reply: QNetworkReply): self._last_response_time = time() try: - self._onFinishedCallbacks[reply.request()](reply) - del self._onFinishedCallbacks[reply.request] # Remove the callback. + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply) except Exception as e: print("Something went wrong with callback", e) pass diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index f4e60b49e4..8de14fe233 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -7,5 +7,3 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def _update(self): super()._update() - - pass diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index 8131493957..d79bd543e7 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -29,6 +29,12 @@ Cura.MachineAction function connectToPrinter() { + if(base.selectedDevice) + { + var deviceKey = base.selectedDevice.key + manager.setKey(deviceKey); + completed(); + } if(base.selectedPrinter && base.completeProperties) { var printerKey = base.selectedDevice.key diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index c7ccbe763a..86211bddb4 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -1,8 +1,9 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice + 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) def _update(self): - pass + super()._update() \ No newline at end of file From 1167fa0a89f9e4e2771aaec6ede3c59b7c9133c8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 16:03:54 +0100 Subject: [PATCH 016/135] Added data handling for legacy printer CL-541 --- .../NetworkedPrinterOutputDevice.py | 8 +- cura/PrinterOutput/PrintJobOutputModel.py | 11 +++ cura/PrinterOutput/PrinterOutputModel.py | 13 ++- .../LegacyUM3OutputDevice.py | 79 ++++++++++++++++++- 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index e33834ffce..951b7138f1 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -1,4 +1,6 @@ from UM.Application import Application +from UM.Logger import Logger + from cura.PrinterOutputDevice import PrinterOutputDevice from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply @@ -30,6 +32,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _createEmptyRequest(self, target): url = QUrl("http://" + self._address + self._api_prefix + target) + print(url) request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) @@ -66,9 +69,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() try: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply) - except Exception as e: - print("Something went wrong with callback", e) - pass + except Exception: + Logger.logException("w", "something went wrong with callback") @pyqtSlot(str, result=str) def getProperty(self, key): diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 1e0d82f1b0..407bffcbfe 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -11,6 +11,7 @@ class PrintJobOutputModel(QObject): stateChanged = pyqtSignal() timeTotalChanged = pyqtSignal() timeElapsedChanged = pyqtSignal() + nameChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", parent=None): super().__init__(parent) @@ -18,6 +19,16 @@ class PrintJobOutputModel(QObject): self._state = "" self._time_total = 0 self._time_elapsed = 0 + self._name = "" + + @pyqtProperty(str, notify = nameChanged) + def name(self): + return self._name + + def updateName(self, name: str): + if self._name != name: + self._name = name + self.nameChanged.emit() @pyqtProperty(int, notify = timeTotalChanged) def timeTotal(self): diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 00644980b4..7c10944cfd 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -5,11 +5,11 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot from UM.Logger import Logger from typing import Optional, List from UM.Math.Vector import Vector +from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel - from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController @@ -21,17 +21,19 @@ class PrinterOutputModel(QObject): nameChanged = pyqtSignal() headPositionChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", extruders: List["ExtruderOutputModel"], parent=None): + def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): super().__init__(parent) self._bed_temperature = 0 self._target_bed_temperature = 0 self._name = "" self._controller = output_controller - self._extruders = extruders + self._extruders = [ExtruderOutputModel(printer=self)] * number_of_extruders self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] + self._printer_state = "unknown" + # Features of the printer; self._can_pause = True self._can_abort = True @@ -135,6 +137,11 @@ class PrinterOutputModel(QObject): self._active_print_job = print_job self.activePrintJobChanged.emit() + def updatePrinterState(self, printer_state): + if self._printer_state != printer_state: + self._printer_state = printer_state + self.printerStateChanged.emit() + @pyqtProperty(QObject, notify = activePrintJobChanged) def activePrintJob(self): return self._active_print_job diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 86211bddb4..b4e7bdf1af 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -1,9 +1,86 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice +from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel + +from UM.Logger import Logger + +from PyQt5.QtNetwork import QNetworkRequest + + +import json 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 def _update(self): - super()._update() \ No newline at end of file + super()._update() + self._get("printer", onFinished=self._onGetPrinterDataFinished) + self._get("print_job", onFinished=self._onGetPrintJobFinished) + + 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=None) + 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 _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: + self._printers = [PrinterOutputModel(output_controller=None, number_of_extruders=self._number_of_extruders)] + + # LegacyUM3 always has a single printer. + printer = self._printers[0] + printer.updateBedTemperature(result["bed"]["temperature"]["current"]) + printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"]) + printer.updatePrinterState(result["status"]) + + for index in range(0, self._number_of_extruders): + temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"] + printer.extruders[index].updateTargetHotendTemperature(temperatures["target"]) + printer.extruders[index].updateHotendTemperature(temperatures["current"]) + + # TODO: Set active 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)) From a9f52c2ad642dd468eb90b9a245d3d8596f79229 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 20 Nov 2017 17:00:02 +0100 Subject: [PATCH 017/135] Added data handling for Connect devices CL-541 --- cura/PrinterOutput/PrintJobOutputModel.py | 13 ++++- cura/PrinterOutput/PrinterOutputModel.py | 11 ++++ .../ClusterUM3OutputDevice.py | 57 ++++++++++++++++++- .../LegacyUM3OutputDevice.py | 1 - 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 407bffcbfe..ca04c546d3 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -12,6 +12,7 @@ class PrintJobOutputModel(QObject): timeTotalChanged = pyqtSignal() timeElapsedChanged = pyqtSignal() nameChanged = pyqtSignal() + keyChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", parent=None): super().__init__(parent) @@ -19,7 +20,17 @@ class PrintJobOutputModel(QObject): self._state = "" self._time_total = 0 self._time_elapsed = 0 - self._name = "" + self._name = "" # Human readable name + self._key = "" # Unique identifier + + @pyqtProperty(str, notify=keyChanged) + def key(self): + return self._key + + def updateKey(self, key: str): + if self._key != key: + self._key = key + self.keyChanged.emit() @pyqtProperty(str, notify = nameChanged) def name(self): diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 7c10944cfd..ed20ef1755 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -20,12 +20,14 @@ class PrinterOutputModel(QObject): activePrintJobChanged = pyqtSignal() nameChanged = pyqtSignal() headPositionChanged = pyqtSignal() + keyChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): super().__init__(parent) self._bed_temperature = 0 self._target_bed_temperature = 0 self._name = "" + self._key = "" # Unique identifier self._controller = output_controller self._extruders = [ExtruderOutputModel(printer=self)] * number_of_extruders @@ -40,6 +42,15 @@ class PrinterOutputModel(QObject): self._can_pre_heat_bed = True self._can_control_manually = True + @pyqtProperty(str, notify=keyChanged) + def key(self): + return self._key + + def updateKey(self, key: str): + if self._key != key: + self._key = key + self.keyChanged.emit() + @pyqtSlot() def homeHead(self): self._controller.homeHead(self) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 8de14fe233..a1c4f48e13 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -1,9 +1,64 @@ -from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice +from UM.Logger import Logger +from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice +from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel +from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel + +import json + +from PyQt5.QtNetwork import QNetworkRequest class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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 def _update(self): super()._update() + self._get("printers/", onFinished=self._onGetPrintersDataFinished) + + def _onGetPrintersDataFinished(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 + + for printer_data in result: + uuid = printer_data["uuid"] + + printer = None + for device in self._printers: + if device.key == uuid: + printer = device + break + + if printer is None: + printer = PrinterOutputModel(output_controller=None, number_of_extruders=self._number_of_extruders) + self._printers.append(printer) + + printer.updateName(printer_data["friendly_name"]) + printer.updateKey(uuid) + + for index in range(0, self._number_of_extruders): + extruder = printer.extruders[index] + extruder_data = printer_data["configuration"][index] + try: + hotend_id = extruder_data["print_core_id"] + except KeyError: + hotend_id = "" + extruder.updateHotendID(hotend_id) + + material_data = extruder_data["material"] + if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: + material = MaterialOutputModel(guid = material_data["guid"], type = material_data["material"], brand=material_data["brand"], color=material_data["color"]) + extruder.updateActiveMaterial(material) + + else: + Logger.log("w", + "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index b4e7bdf1af..63ebd055ad 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -6,7 +6,6 @@ from UM.Logger import Logger from PyQt5.QtNetwork import QNetworkRequest - import json From fd548975ccfd62bb308ed37e8aae83d1a07b42be Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 10:19:17 +0100 Subject: [PATCH 018/135] Closing a connection now actually stops the updates CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 4 +++- cura/PrinterOutputDevice.py | 3 +++ plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 951b7138f1..2c33f2e397 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -1,7 +1,7 @@ from UM.Application import Application from UM.Logger import Logger -from cura.PrinterOutputDevice import PrinterOutputDevice +from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl @@ -67,6 +67,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def __handleOnFinished(self, reply: QNetworkReply): self._last_response_time = time() + # TODO: Check if the message is actually correct + self.setConnectionState(ConnectionState.connected) try: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply) except Exception: diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 573fe63158..f5afb0da6a 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -54,6 +54,9 @@ class PrinterOutputDevice(QObject, OutputDevice): def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error + def setConnectionState(self, new_state): + self._connection_state = new_state + def _update(self): pass diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 0d1154e07c..b4ea1663b6 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -76,8 +76,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): else: if self._discovered_devices[key].isConnected(): Logger.log("d", "Attempting to close connection with [%s]" % key) - self._printers[key].close() - self._printers[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + self._discovered_devices[key].close() + self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) def _onDeviceConnectionStateChanged(self, key): pass # TODO From 10a2dbb134ae0d42dc0cfdc115e65da065a16d2e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 10:24:14 +0100 Subject: [PATCH 019/135] Extended the typing for the calllbacks CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 2c33f2e397..75f5ca6a14 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -38,20 +38,20 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - def _put(self, target: str, data: str, onFinished: Callable): + def _put(self, target: str, data: str, onFinished: Callable[[QNetworkReply], None]): request = self._createEmptyRequest(target) reply = self._manager.put(request, data.encode()) self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _get(self, target: str, onFinished: Callable): + def _get(self, target: str, onFinished: Callable[[QNetworkReply], None]): request = self._createEmptyRequest(target) reply = self._manager.get(request) self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _delete(self, target: str, onFinished: Callable): + def _delete(self, target: str, onFinished: Callable[[QNetworkReply], None]): pass - def _post(self, target: str, data: str, onFinished: Callable, onProgress: Callable): + def _post(self, target: str, data: str, onFinished: Callable[[QNetworkReply], None], onProgress: Callable): pass def _createNetworkManager(self): From 152f3462ce228825731e35de4c8cc03dcd3bbefb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 11:00:37 +0100 Subject: [PATCH 020/135] Also added any to callable mypy decorator For some reason it also wants to know that it also calls self. Weird. CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 75f5ca6a14..7b74282303 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -7,7 +7,7 @@ from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetwork from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl from time import time -from typing import Callable +from typing import Callable, Any class NetworkedPrinterOutputDevice(PrinterOutputDevice): @@ -38,20 +38,20 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - def _put(self, target: str, data: str, onFinished: Callable[[QNetworkReply], None]): + def _put(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None]): request = self._createEmptyRequest(target) reply = self._manager.put(request, data.encode()) self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _get(self, target: str, onFinished: Callable[[QNetworkReply], None]): + def _get(self, target: str, onFinished: Callable[[Any, QNetworkReply], None]): request = self._createEmptyRequest(target) reply = self._manager.get(request) self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _delete(self, target: str, onFinished: Callable[[QNetworkReply], None]): + def _delete(self, target: str, onFinished: Callable[[Any, QNetworkReply], None]): pass - def _post(self, target: str, data: str, onFinished: Callable[[QNetworkReply], None], onProgress: Callable): + def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable): pass def _createNetworkManager(self): From b1649f2d38d8f2bbe5c2de4f20743199f55317a6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 11:01:19 +0100 Subject: [PATCH 021/135] Added PrintJob handling to ClusterUM3 CL-541 --- cura/PrinterOutput/PrintJobOutputModel.py | 16 ++++++- cura/PrinterOutput/PrinterOutputModel.py | 4 ++ cura/PrinterOutputDevice.py | 7 +++ .../ClusterUM3OutputDevice.py | 46 +++++++++++++++++-- 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index ca04c546d3..7c38782788 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -5,6 +5,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant MYPY = False if MYPY: from cura.PrinterOutput.PrinterOutputController import PrinterOutputController + from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel class PrintJobOutputModel(QObject): @@ -13,15 +14,26 @@ class PrintJobOutputModel(QObject): timeElapsedChanged = pyqtSignal() nameChanged = pyqtSignal() keyChanged = pyqtSignal() + assignedPrinterChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", parent=None): + def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None): super().__init__(parent) self._output_controller = output_controller self._state = "" self._time_total = 0 self._time_elapsed = 0 self._name = "" # Human readable name - self._key = "" # Unique identifier + self._key = key # Unique identifier + self._assigned_printer = None + + @pyqtProperty(QObject, notify=assignedPrinterChanged) + def assignedPrinter(self): + return self._assigned_printer + + def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"): + if self._assigned_printer != assigned_printer: + self._assigned_printer = assigned_printer + self.assignedPrinterChanged.emit() @pyqtProperty(str, notify=keyChanged) def key(self): diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index ed20ef1755..8a5a9b55be 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -145,6 +145,10 @@ class PrinterOutputModel(QObject): def updateActivePrintJob(self, print_job): if self._active_print_job != print_job: + if self._active_print_job is not None: + self._active_print_job.updateAssignedPrinter(None) + if print_job is not None: + print_job.updateAssignedPrinter(self) self._active_print_job = print_job self.activePrintJobChanged.emit() diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index f5afb0da6a..6de665b67f 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -60,6 +60,13 @@ class PrinterOutputDevice(QObject, OutputDevice): def _update(self): pass + def _getPrinterByKey(self, key): + for printer in self._printers: + if printer.key == key: + return printer + + return None + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): raise NotImplementedError("requestWrite needs to be implemented") diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index a1c4f48e13..6e564fef29 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -7,7 +7,7 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel import json -from PyQt5.QtNetwork import QNetworkRequest +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address, properties, parent = None): @@ -16,17 +16,57 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._number_of_extruders = 2 + self._print_jobs = [] + def _update(self): super()._update() self._get("printers/", onFinished=self._onGetPrintersDataFinished) + self._get("print_jobs/", onFinished=self._onGetPrintJobsFinished) - def _onGetPrintersDataFinished(self, reply): + def _onGetPrintJobsFinished(self, reply: QNetworkReply): 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.") + Logger.log("w", "Received an invalid print jobs message: Not valid JSON.") + return + print_jobs_seen = [] + for print_job_data in result: + print_job = None + for job in self._print_jobs: + if job.key == print_job_data["uuid"]: + print_job = job + break + + if print_job is None: + print_job = PrintJobOutputModel(output_controller = None, + key = print_job_data["uuid"], + name = print_job_data["name"]) + print_job.updateTimeTotal(print_job_data["time_total"]) + print_job.updateTimeElapsed(print_job_data["time_elapsed"]) + print_job.updateState(print_job_data["status"]) + if print_job.state == "printing": + # Print job should be assigned to a printer. + printer = self._getPrinterByKey(print_job_data["printer_uuid"]) + if printer: + printer.updateActivePrintJob(print_job) + + print_jobs_seen.append(print_job) + for old_job in self._print_jobs: + if old_job not in print_jobs_seen: + # Print job needs to be removed. + old_job.assignedPrinter.updateActivePrintJob(None) + + self._print_jobs = print_jobs_seen + + def _onGetPrintersDataFinished(self, reply: QNetworkReply): + 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 printers state message: Not valid JSON.") return for printer_data in result: From a8e71cf50cf04b7fe8ea2cb91a1b3b57f54a4475 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 14:35:17 +0100 Subject: [PATCH 022/135] NetworkManager is now created on demand and re-created after a certain timeout. CL-541 --- .../NetworkedPrinterOutputDevice.py | 52 ++++++++++++++++--- .../ClusterUM3OutputDevice.py | 3 +- .../LegacyUM3OutputDevice.py | 3 +- .../UM3OutputDevicePlugin.py | 8 ++- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 7b74282303..3330426d0a 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -14,9 +14,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent = None): super().__init__(device_id = device_id, parent = parent) self._manager = None - self._createNetworkManager() - self._last_response_time = time() + self._last_manager_create_time = None + self._recreate_network_manager_time = 30 + self._timeout_time = 10 # After how many seconds of no response should a timeout occur? + + self._last_response_time = None self._last_request_time = None + self._api_prefix = "" self._address = address self._properties = properties @@ -25,10 +29,28 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._onFinishedCallbacks = {} def _update(self): - if not self._manager.networkAccessible(): - pass # TODO: no internet connection. + if self._last_response_time: + time_since_last_response = time() - self._last_response_time + else: + time_since_last_response = 0 - pass + if self._last_request_time: + time_since_last_request = time() - self._last_request_time + else: + time_since_last_request = float("inf") # An irrelevantly large number of seconds + + if time_since_last_response > self._timeout_time >= time_since_last_request: + # Go (or stay) into timeout. + self.setConnectionState(ConnectionState.closed) + # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to + # sleep. + if time_since_last_response > self._recreate_network_manager_time: + if self._last_manager_create_time is None: + self._createNetworkManager() + if time() - self._last_manager_create_time > self._recreate_network_manager_time: + self._createNetworkManager() + + return True def _createEmptyRequest(self, target): url = QUrl("http://" + self._address + self._api_prefix + target) @@ -39,22 +61,35 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): return request def _put(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None]): + if self._manager is None: + self._createNetworkManager() request = self._createEmptyRequest(target) + self._last_request_time = time() reply = self._manager.put(request, data.encode()) self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished def _get(self, target: str, onFinished: Callable[[Any, QNetworkReply], None]): + if self._manager is None: + self._createNetworkManager() request = self._createEmptyRequest(target) + self._last_request_time = time() reply = self._manager.get(request) self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished def _delete(self, target: str, onFinished: Callable[[Any, QNetworkReply], None]): + if self._manager is None: + self._createNetworkManager() + self._last_request_time = time() pass def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable): + if self._manager is None: + self._createNetworkManager() + self._last_request_time = time() pass def _createNetworkManager(self): + Logger.log("d", "Creating network manager") if self._manager: self._manager.finished.disconnect(self.__handleOnFinished) #self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) @@ -62,12 +97,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._manager = QNetworkAccessManager() self._manager.finished.connect(self.__handleOnFinished) + self._last_manager_create_time = time() #self._manager.authenticationRequired.connect(self._onAuthenticationRequired) #self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes def __handleOnFinished(self, reply: QNetworkReply): + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: + # No status code means it never even reached remote. + return + self._last_response_time = time() - # TODO: Check if the message is actually correct + self.setConnectionState(ConnectionState.connected) try: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 6e564fef29..8f9a92384f 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -19,7 +19,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._print_jobs = [] def _update(self): - super()._update() + if not super()._update(): + return self._get("printers/", onFinished=self._onGetPrintersDataFinished) self._get("print_jobs/", onFinished=self._onGetPrintJobsFinished) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 63ebd055ad..21b58154a6 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -16,7 +16,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._number_of_extruders = 2 def _update(self): - super()._update() + if not super()._update(): + return self._get("printer", onFinished=self._onGetPrinterDataFinished) self._get("print_job", onFinished=self._onGetPrintJobFinished) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index b4ea1663b6..1462fb9373 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -80,7 +80,13 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) def _onDeviceConnectionStateChanged(self, key): - pass # TODO + 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: From 3f1167a7d2953447b03301427aa380dc92068033 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 14:39:03 +0100 Subject: [PATCH 023/135] Results in printer discovery are sorted again CL-541 --- plugins/UM3NetworkPrinting/DiscoverUM3Action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index f7afe3e00f..3c2a37e0a4 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -93,7 +93,7 @@ class DiscoverUM3Action(MachineAction): 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.sort(key = lambda k: k.name) + printers.sort(key = lambda k: k.name) return printers else: return [] From 9cfe9769d318ead92c776626c3b6b010582094b6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 14:47:55 +0100 Subject: [PATCH 024/135] Printers now automatically try to connect again CL-541 --- .../NetworkedPrinterOutputDevice.py | 1 - .../UM3OutputDevicePlugin.py | 23 ++++--------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 3330426d0a..d2886328de 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -54,7 +54,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _createEmptyRequest(self, target): url = QUrl("http://" + self._address + self._api_prefix + target) - print(url) request = QNetworkRequest(url) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 1462fb9373..98fab42a44 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -32,6 +32,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self.addDeviceSignal.connect(self._onAddDevice) self.removeDeviceSignal.connect(self._onRemoveDevice) + Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections) + self._discovered_devices = {} # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests @@ -101,16 +103,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) self.discoveredDevicesChanged.emit() - '''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()''' 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)) @@ -122,17 +116,10 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._discovered_devices[device.getId()] = device self.discoveredDevicesChanged.emit() - pass - ''' - 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()''' + 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): From 0f78b05802b19e259ad7a66fb8500c41b979199b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 15:12:23 +0100 Subject: [PATCH 025/135] Connection states changes are now tied into the UI again CL-541 --- cura/PrinterOutputDevice.py | 8 +++++--- cura/Settings/MachineManager.py | 8 ++++---- plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py | 12 +++++++----- resources/qml/PrintMonitor.qml | 4 +++- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 6de665b67f..56ac318f20 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -27,7 +27,7 @@ i18n_catalog = i18nCatalog("cura") @signalemitter class PrinterOutputDevice(QObject, OutputDevice): printersChanged = pyqtSignal() - connectionStateChanged = pyqtSignal() + connectionStateChanged = pyqtSignal(str) def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) @@ -54,8 +54,10 @@ class PrinterOutputDevice(QObject, OutputDevice): def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error - def setConnectionState(self, new_state): - self._connection_state = new_state + def setConnectionState(self, connection_state): + if self._connection_state != connection_state: + self._connection_state = connection_state + self.connectionStateChanged.emit(self._id) def _update(self): pass diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 0daf54c018..780a2a05ad 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -133,17 +133,17 @@ class MachineManager(QObject): outputDevicesChanged = pyqtSignal() def _onOutputDevicesChanged(self) -> None: - for printer_output_device in self._printer_output_devices: + '''for printer_output_device in self._printer_output_devices: printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged) - printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged) + printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)''' self._printer_output_devices.clear() for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) - printer_output_device.hotendIdChanged.connect(self._onHotendIdChanged) - printer_output_device.materialIdChanged.connect(self._onMaterialIdChanged) + #printer_output_device.hotendIdChanged.connect(self._onHotendIdChanged) + #printer_output_device.materialIdChanged.connect(self._onMaterialIdChanged) self.outputDevicesChanged.emit() diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 98fab42a44..09bff8e7b8 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -84,7 +84,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): def _onDeviceConnectionStateChanged(self, key): if key not in self._discovered_devices: return - + print("STATE CHANGED", key) if self._discovered_devices[key].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key]) else: @@ -95,8 +95,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): Logger.log("d", "zeroconf close...") self._zero_conf.close() - def _onRemoveDevice(self, name): - device = self._discovered_devices.pop(name, None) + def _onRemoveDevice(self, device_id): + device = self._discovered_devices.pop(device_id, None) if device: if device.isConnected(): device.disconnect() @@ -108,10 +108,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # 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: + # TODO: For debug purposes; force it to be legacy printer. + device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) + '''if cluster_size > 0: device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) else: - device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) + device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)''' self._discovered_devices[device.getId()] = device self.discoveredDevicesChanged.emit() diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index e69f7cf4fd..901c8f9fdc 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -12,7 +12,9 @@ import Cura 1.0 as Cura Column { id: printMonitor - property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null + property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null + + property var activePrinter: connectedDevice != null ? connectedDevice.activePrinter : null Cura.ExtrudersModel { From e3d07f1806bf546eb1dcfafc7b9e691c52bcbf7d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 16:06:30 +0100 Subject: [PATCH 026/135] Moved ExtruderBox and OutputDeviceHeader to their own files. This makes it a whole lot easier to get an overview. CL-541 --- cura/PrinterOutput/ExtruderOuputModel.py | 2 +- resources/qml/PrintMonitor.qml | 223 +----------------- resources/qml/PrinterOutput/ExtruderBox.qml | 201 ++++++++++++++++ .../qml/PrinterOutput/OutputDeviceHeader.qml | 54 +++++ 4 files changed, 263 insertions(+), 217 deletions(-) create mode 100644 resources/qml/PrinterOutput/ExtruderBox.qml create mode 100644 resources/qml/PrinterOutput/OutputDeviceHeader.qml diff --git a/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOuputModel.py index 121e9a69d9..f8f8088389 100644 --- a/cura/PrinterOutput/ExtruderOuputModel.py +++ b/cura/PrinterOutput/ExtruderOuputModel.py @@ -58,7 +58,7 @@ class ExtruderOutputModel(QObject): @pyqtProperty(int, notify=hotendTemperatureChanged) def hotendTemperature(self) -> int: - return self._hotendTemperature + return self._hotend_temperature @pyqtProperty(str, notify = hotendIDChanged) def hotendID(self) -> str: diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 901c8f9fdc..6c815827f6 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -9,6 +9,8 @@ import QtQuick.Layouts 1.1 import UM 1.2 as UM import Cura 1.0 as Cura +import "PrinterOutput" + Column { id: printMonitor @@ -22,45 +24,10 @@ Column simpleNames: true } - Rectangle + OutputDeviceHeader { - id: connectedPrinterHeader width: parent.width - height: Math.floor(childrenRect.height + UM.Theme.getSize("default_margin").height * 2) - color: UM.Theme.getColor("setting_category") - - Label - { - id: connectedPrinterNameLabel - font: UM.Theme.getFont("large") - color: UM.Theme.getColor("text") - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: UM.Theme.getSize("default_margin").width - text: connectedPrinter != null ? connectedPrinter.name : catalog.i18nc("@info:status", "No printer connected") - } - Label - { - id: connectedPrinterAddressLabel - text: (connectedPrinter != null && connectedPrinter.address != null) ? connectedPrinter.address : "" - font: UM.Theme.getFont("small") - color: UM.Theme.getColor("text_inactive") - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: UM.Theme.getSize("default_margin").width - } - Label - { - text: connectedPrinter != null ? connectedPrinter.connectionText : catalog.i18nc("@info:status", "The printer is not connected.") - color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") - font: UM.Theme.getFont("very_small") - wrapMode: Text.WordWrap - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.top: connectedPrinterNameLabel.bottom - } + outputDevice: connectedDevice } Rectangle @@ -78,189 +45,13 @@ Column Repeater { id: extrudersRepeater - model: machineExtruderCount.properties.value + model: activePrinter.extruders - delegate: Rectangle + ExtruderBox { - id: extruderRectangle color: UM.Theme.getColor("sidebar") width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) - height: UM.Theme.getSize("sidebar_extruder_box").height - - Label //Extruder name. - { - text: Cura.ExtruderManager.getExtruderName(index) != "" ? Cura.ExtruderManager.getExtruderName(index) : catalog.i18nc("@label", "Extruder") - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default") - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: UM.Theme.getSize("default_margin").width - } - - Label //Target temperature. - { - id: extruderTargetTemperature - text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.targetHotendTemperatures[index] != null) ? Math.round(connectedPrinter.targetHotendTemperatures[index]) + "°C" : "" - font: UM.Theme.getFont("small") - color: UM.Theme.getColor("text_inactive") - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.bottom: extruderTemperature.bottom - - MouseArea //For tooltip. - { - id: extruderTargetTemperatureTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: extruderTargetTemperature.mapToItem(base, 0, -parent.height / 4).y}, - catalog.i18nc("@tooltip", "The target temperature of the hotend. The hotend will heat up or cool down towards this temperature. If this is 0, the hotend heating is turned off.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } - Label //Temperature indication. - { - id: extruderTemperature - text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.hotendTemperatures[index] != null) ? Math.round(connectedPrinter.hotendTemperatures[index]) + "°C" : "" - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("large") - anchors.right: extruderTargetTemperature.left - anchors.top: parent.top - anchors.margins: UM.Theme.getSize("default_margin").width - - MouseArea //For tooltip. - { - id: extruderTemperatureTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y}, - catalog.i18nc("@tooltip", "The current temperature of this extruder.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } - Rectangle //Material colour indication. - { - id: materialColor - width: Math.floor(materialName.height * 0.75) - height: Math.floor(materialName.height * 0.75) - radius: width / 2 - color: (connectedPrinter != null && connectedPrinter.materialColors[index] != null && connectedPrinter.materialIds[index] != "") ? connectedPrinter.materialColors[index] : "#00000000" - border.width: UM.Theme.getSize("default_lining").width - border.color: UM.Theme.getColor("lining") - visible: connectedPrinter != null && connectedPrinter.materialColors[index] != null && connectedPrinter.materialIds[index] != "" - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: materialName.verticalCenter - - MouseArea //For tooltip. - { - id: materialColorTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: parent.mapToItem(base, 0, -parent.height / 2).y}, - catalog.i18nc("@tooltip", "The colour of the material in this extruder.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } - Label //Material name. - { - id: materialName - text: (connectedPrinter != null && connectedPrinter.materialNames[index] != null && connectedPrinter.materialIds[index] != "") ? connectedPrinter.materialNames[index] : "" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - anchors.left: materialColor.right - anchors.bottom: parent.bottom - anchors.margins: UM.Theme.getSize("default_margin").width - - MouseArea //For tooltip. - { - id: materialNameTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: parent.mapToItem(base, 0, 0).y}, - catalog.i18nc("@tooltip", "The material in this extruder.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } - Label //Variant name. - { - id: variantName - text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null) ? connectedPrinter.hotendIds[index] : "" - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: UM.Theme.getSize("default_margin").width - - MouseArea //For tooltip. - { - id: variantNameTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y}, - catalog.i18nc("@tooltip", "The nozzle inserted in this extruder.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } + extruderModel: activePrinter.extruders[index] } } } diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml new file mode 100644 index 0000000000..2860789dd0 --- /dev/null +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -0,0 +1,201 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 + +import UM 1.2 as UM +import Cura 1.0 as Cura + + +Item +{ + property alias color: background.color + property var extruderModel + property var position: index + //width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) + implicitWidth: parent.width + implicitHeight: UM.Theme.getSize("sidebar_extruder_box").height + Rectangle + { + id: background + anchors.fill: parent + + Label //Extruder name. + { + text: Cura.ExtruderManager.getExtruderName(position) != "" ? Cura.ExtruderManager.getExtruderName(position) : catalog.i18nc("@label", "Extruder") + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("default") + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: UM.Theme.getSize("default_margin").width + } + + Label //Target temperature. + { + id: extruderTargetTemperature + text: Math.round(extruderModel.targetHotendTemperature) + "°C" + //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.targetHotendTemperatures[index] != null) ? Math.round(connectedPrinter.targetHotendTemperatures[index]) + "°C" : "" + font: UM.Theme.getFont("small") + color: UM.Theme.getColor("text_inactive") + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.bottom: extruderTemperature.bottom + + MouseArea //For tooltip. + { + id: extruderTargetTemperatureTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: extruderTargetTemperature.mapToItem(base, 0, -parent.height / 4).y}, + catalog.i18nc("@tooltip", "The target temperature of the hotend. The hotend will heat up or cool down towards this temperature. If this is 0, the hotend heating is turned off.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + Label //Temperature indication. + { + id: extruderTemperature + text: Math.round(extruderModel.hotendTemperature) + "°C" + //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.hotendTemperatures[index] != null) ? Math.round(connectedPrinter.hotendTemperatures[index]) + "°C" : "" + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("large") + anchors.right: extruderTargetTemperature.left + anchors.top: parent.top + anchors.margins: UM.Theme.getSize("default_margin").width + + MouseArea //For tooltip. + { + id: extruderTemperatureTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y}, + catalog.i18nc("@tooltip", "The current temperature of this extruder.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + + Rectangle //Material colour indication. + { + id: materialColor + width: Math.floor(materialName.height * 0.75) + height: Math.floor(materialName.height * 0.75) + radius: width / 2 + color: extruderModel.activeMaterial ? extruderModel.activeMaterial.color: "#00000000" + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("lining") + visible: extruderModel.activeMaterial != null + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: materialName.verticalCenter + + MouseArea //For tooltip. + { + id: materialColorTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: parent.mapToItem(base, 0, -parent.height / 2).y}, + catalog.i18nc("@tooltip", "The colour of the material in this extruder.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + Label //Material name. + { + id: materialName + text: extruderModel.activeMaterial != null ? extruderModel.activeMaterial.name : "" + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + anchors.left: materialColor.right + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").width + + MouseArea //For tooltip. + { + id: materialNameTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: parent.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip", "The material in this extruder.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + Label //Variant name. + { + id: variantName + text: extruderModel.hotendID + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").width + + MouseArea //For tooltip. + { + id: variantNameTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y}, + catalog.i18nc("@tooltip", "The nozzle inserted in this extruder.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + } +} \ No newline at end of file diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml new file mode 100644 index 0000000000..6553655da0 --- /dev/null +++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml @@ -0,0 +1,54 @@ +import QtQuick 2.2 + +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 + +import UM 1.2 as UM +import Cura 1.0 as Cura + + +Item +{ + implicitWidth: parent.width + implicitHeight: Math.floor(childrenRect.height + UM.Theme.getSize("default_margin").height * 2) + property var outputDevice: null + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("setting_category") + + Label + { + id: outputDeviceNameLabel + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: UM.Theme.getSize("default_margin").width + text: outputDevice != null ? outputDevice.name : catalog.i18nc("@info:status", "No printer connected") + } + Label + { + id: outputDeviceAddressLabel + text: (outputDevice != null && outputDevice.address != null) ? outputDevice.address : "" + font: UM.Theme.getFont("small") + color: UM.Theme.getColor("text_inactive") + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: UM.Theme.getSize("default_margin").width + } + Label + { + text: outputDevice != null ? outputDevice.connectionText : catalog.i18nc("@info:status", "The printer is not connected.") + color: outputDevice != null && outputDevice.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") + font: UM.Theme.getFont("very_small") + wrapMode: Text.WordWrap + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.top: outputDevice.bottom + } + } +} \ No newline at end of file From d8b12be5e4410228704461b5293c7ba9898cc285 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 16:26:17 +0100 Subject: [PATCH 027/135] LegacyUM3 now correctly gets material set CL-541 --- .../LegacyUM3OutputDevice.py | 26 ++++++++++++++++--- resources/qml/PrinterOutput/ExtruderBox.qml | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 21b58154a6..60409ec729 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -1,8 +1,10 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel +from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from UM.Logger import Logger +from UM.Settings.ContainerRegistry import ContainerRegistry from PyQt5.QtNetwork import QNetworkRequest @@ -70,10 +72,28 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): for index in range(0, self._number_of_extruders): temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"] - printer.extruders[index].updateTargetHotendTemperature(temperatures["target"]) - printer.extruders[index].updateHotendTemperature(temperatures["current"]) + extruder = printer.extruders[index] + extruder.updateTargetHotendTemperature(temperatures["target"]) + extruder.updateHotendTemperature(temperatures["current"]) - # TODO: Set active material + 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") + else: + # Unknown material. + color = "#00000000" + brand = "Unknown" + material_type = "Unknown" + material = MaterialOutputModel(guid=material_guid, type=material_type, + brand=brand, color=color) + extruder.updateActiveMaterial(material) try: hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"] diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index 2860789dd0..a7141262a9 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -136,7 +136,7 @@ Item Label //Material name. { id: materialName - text: extruderModel.activeMaterial != null ? extruderModel.activeMaterial.name : "" + text: extruderModel.activeMaterial != null ? extruderModel.activeMaterial.type : "" font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") anchors.left: materialColor.right From 34e808d585cb1f6e74a74d012430c96850f6efe6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 16:36:51 +0100 Subject: [PATCH 028/135] PrinterOutputModel now has different extruders if it has more than one. It used to just fill the list with references to the first one created. CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 3 +-- resources/qml/PrintMonitor.qml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 8a5a9b55be..9c1040fe1b 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -29,8 +29,7 @@ class PrinterOutputModel(QObject): self._name = "" self._key = "" # Unique identifier self._controller = output_controller - self._extruders = [ExtruderOutputModel(printer=self)] * number_of_extruders - + self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)] self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 6c815827f6..23ab365861 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -51,7 +51,7 @@ Column { color: UM.Theme.getColor("sidebar") width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) - extruderModel: activePrinter.extruders[index] + extruderModel: modelData } } } From 0fe91db6362e3254fa3b7adb5a8084be61e32419 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 16:52:37 +0100 Subject: [PATCH 029/135] Moved HeatedBedBox to own qml file CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 2 +- resources/qml/PrintMonitor.qml | 394 +----------------- resources/qml/PrinterOutput/HeatedBedBox.qml | 399 +++++++++++++++++++ 3 files changed, 409 insertions(+), 386 deletions(-) create mode 100644 resources/qml/PrinterOutput/HeatedBedBox.qml diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 9c1040fe1b..12c2b4fe58 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -24,7 +24,7 @@ class PrinterOutputModel(QObject): def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): super().__init__(parent) - self._bed_temperature = 0 + self._bed_temperature = -1 # Use -1 for no heated bed. self._target_bed_temperature = 0 self._name = "" self._key = "" # Unique identifier diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 23ab365861..f95d829306 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -15,7 +15,6 @@ Column { id: printMonitor property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - property var activePrinter: connectedDevice != null ? connectedDevice.activePrinter : null Cura.ExtrudersModel @@ -51,7 +50,7 @@ Column { color: UM.Theme.getColor("sidebar") width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) - extruderModel: modelData + extruderModel: modelData } } } @@ -64,391 +63,16 @@ Column height: UM.Theme.getSize("sidebar_lining_thin").width } - Rectangle + HeatedBedBox { - color: UM.Theme.getColor("sidebar") - width: parent.width - height: machineHeatedBed.properties.value == "True" ? UM.Theme.getSize("sidebar_extruder_box").height : 0 - visible: machineHeatedBed.properties.value == "True" - - Label //Build plate label. - { - text: catalog.i18nc("@label", "Build plate") - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: UM.Theme.getSize("default_margin").width - } - Label //Target temperature. - { - id: bedTargetTemperature - text: connectedPrinter != null ? connectedPrinter.targetBedTemperature + "°C" : "" - font: UM.Theme.getFont("small") - color: UM.Theme.getColor("text_inactive") - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.bottom: bedCurrentTemperature.bottom - - MouseArea //For tooltip. - { - id: bedTargetTemperatureTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: bedTargetTemperature.mapToItem(base, 0, -parent.height / 4).y}, - catalog.i18nc("@tooltip", "The target temperature of the heated bed. The bed will heat up or cool down towards this temperature. If this is 0, the bed heating is turned off.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } - Label //Current temperature. - { - id: bedCurrentTemperature - text: connectedPrinter != null ? connectedPrinter.bedTemperature + "°C" : "" - font: UM.Theme.getFont("large") - color: UM.Theme.getColor("text") - anchors.right: bedTargetTemperature.left - anchors.top: parent.top - anchors.margins: UM.Theme.getSize("default_margin").width - - MouseArea //For tooltip. - { - id: bedTemperatureTooltipArea - hoverEnabled: true - anchors.fill: parent - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: bedCurrentTemperature.mapToItem(base, 0, -parent.height / 4).y}, - catalog.i18nc("@tooltip", "The current temperature of the heated bed.") - ); - } - else - { - base.hideTooltip(); - } - } - } - } - Rectangle //Input field for pre-heat temperature. - { - id: preheatTemperatureControl - color: !enabled ? UM.Theme.getColor("setting_control_disabled") : showError ? UM.Theme.getColor("setting_validation_error_background") : UM.Theme.getColor("setting_validation_ok") - property var showError: - { - if(bedTemperature.properties.maximum_value != "None" && bedTemperature.properties.maximum_value < Math.floor(preheatTemperatureInput.text)) - { - return true; - } else - { - return false; - } - } - enabled: - { - if (connectedPrinter == null) - { - return false; //Can't preheat if not connected. - } - if (!connectedPrinter.acceptsCommands) - { - return false; //Not allowed to do anything. - } - if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") - { - return false; //Printer is in a state where it can't react to pre-heating. - } - return true; - } - border.width: UM.Theme.getSize("default_lining").width - border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.bottom: parent.bottom - anchors.bottomMargin: UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("setting_control").width - height: UM.Theme.getSize("setting_control").height - visible: connectedPrinter != null ? connectedPrinter.canPreHeatBed: true - Rectangle //Highlight of input field. - { - anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_lining").width - color: UM.Theme.getColor("setting_control_highlight") - opacity: preheatTemperatureControl.hovered ? 1.0 : 0 - } - Label //Maximum temperature indication. - { - text: (bedTemperature.properties.maximum_value != "None" ? bedTemperature.properties.maximum_value : "") + "°C" - color: UM.Theme.getColor("setting_unit") - font: UM.Theme.getFont("default") - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width - anchors.verticalCenter: parent.verticalCenter - } - MouseArea //Change cursor on hovering. - { - id: preheatTemperatureInputMouseArea - hoverEnabled: true - anchors.fill: parent - cursorShape: Qt.IBeamCursor - - onHoveredChanged: - { - if (containsMouse) - { - base.showTooltip( - base, - {x: 0, y: preheatTemperatureInputMouseArea.mapToItem(base, 0, 0).y}, - catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the bed to.") - ); - } - else - { - base.hideTooltip(); - } - } - } - TextInput - { - id: preheatTemperatureInput - font: UM.Theme.getFont("default") - color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") - selectByMouse: true - maximumLength: 10 - enabled: parent.enabled - validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - renderType: Text.NativeRendering - - Component.onCompleted: - { - if (!bedTemperature.properties.value) - { - text = ""; - } - if ((bedTemperature.resolve != "None" && bedTemperature.resolve) && (bedTemperature.stackLevels[0] != 0) && (bedTemperature.stackLevels[0] != 1)) - { - // We have a resolve function. Indicates that the setting is not settable per extruder and that - // we have to choose between the resolved value (default) and the global value - // (if user has explicitly set this). - text = bedTemperature.resolve; - } - else - { - text = bedTemperature.properties.value; - } - } - } - } - - UM.RecolorImage - { - id: preheatCountdownIcon - width: UM.Theme.getSize("save_button_specs_icons").width - height: UM.Theme.getSize("save_button_specs_icons").height - sourceSize.width: width - sourceSize.height: height - color: UM.Theme.getColor("text") - visible: preheatCountdown.visible - source: UM.Theme.getIcon("print_time") - anchors.right: preheatCountdown.left - anchors.rightMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) - anchors.verticalCenter: preheatCountdown.verticalCenter - } - - Timer - { - id: preheatUpdateTimer - interval: 100 //Update every 100ms. You want to update every 1s, but then you have one timer for the updating running out of sync with the actual date timer and you might skip seconds. - running: connectedPrinter != null && connectedPrinter.preheatBedRemainingTime != "" - repeat: true - onTriggered: update() - property var endTime: new Date() //Set initial endTime to be the current date, so that the endTime has initially already passed and the timer text becomes invisible if you were to update. - function update() - { - preheatCountdown.text = "" - if (connectedPrinter != null) - { - preheatCountdown.text = connectedPrinter.preheatBedRemainingTime; - } - if (preheatCountdown.text == "") //Either time elapsed or not connected. - { - stop(); - } - } - } - Label - { - id: preheatCountdown - text: connectedPrinter != null ? connectedPrinter.preheatBedRemainingTime : "" - visible: text != "" //Has no direct effect, but just so that we can link visibility of clock icon to visibility of the countdown text. - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - anchors.right: preheatButton.left - anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: preheatButton.verticalCenter - } - - Button //The pre-heat button. - { - id: preheatButton - height: UM.Theme.getSize("setting_control").height - visible: connectedPrinter != null ? connectedPrinter.canPreHeatBed: true - enabled: - { - if (!preheatTemperatureControl.enabled) - { - return false; //Not connected, not authenticated or printer is busy. - } - if (preheatUpdateTimer.running) - { - return true; //Can always cancel if the timer is running. - } - if (bedTemperature.properties.minimum_value != "None" && Math.floor(preheatTemperatureInput.text) < Math.floor(bedTemperature.properties.minimum_value)) - { - return false; //Target temperature too low. - } - if (bedTemperature.properties.maximum_value != "None" && Math.floor(preheatTemperatureInput.text) > Math.floor(bedTemperature.properties.maximum_value)) - { - return false; //Target temperature too high. - } - if (Math.floor(preheatTemperatureInput.text) == 0) - { - return false; //Setting the temperature to 0 is not allowed (since that cancels the pre-heating). - } - return true; //Preconditions are met. - } - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: UM.Theme.getSize("default_margin").width - style: ButtonStyle { - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("default_margin").width * 2) - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_border"); - } - else - { - return UM.Theme.getColor("action_button_border"); - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered"); - } - else - { - return UM.Theme.getColor("action_button"); - } - } - Behavior on color - { - ColorAnimation - { - duration: 50 - } - } - - Label - { - id: actualLabel - anchors.centerIn: parent - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_text"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_text"); - } - else - { - return UM.Theme.getColor("action_button_text"); - } - } - font: UM.Theme.getFont("action_button") - text: preheatUpdateTimer.running ? catalog.i18nc("@button Cancel pre-heating", "Cancel") : catalog.i18nc("@button", "Pre-heat") - } - } - } - - onClicked: - { - if (!preheatUpdateTimer.running) - { - connectedPrinter.preheatBed(preheatTemperatureInput.text, connectedPrinter.preheatBedTimeout); - preheatUpdateTimer.start(); - preheatUpdateTimer.update(); //Update once before the first timer is triggered. - } - else - { - connectedPrinter.cancelPreheatBed(); - preheatUpdateTimer.update(); - } - } - - onHoveredChanged: - { - if (hovered) - { - base.showTooltip( - base, - {x: 0, y: preheatButton.mapToItem(base, 0, 0).y}, - catalog.i18nc("@tooltip of pre-heat", "Heat the bed in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the bed to heat up when you're ready to print.") - ); - } - else - { - base.hideTooltip(); - } - } + visible: { + if(activePrinter != null && activePrinter.bed_temperature != -1) + { + return true + } + return false } + printerModel: activePrinter } UM.SettingPropertyProvider diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml new file mode 100644 index 0000000000..6ff48df6a2 --- /dev/null +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -0,0 +1,399 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Item +{ + implicitWidth: parent.width + height: visible ? UM.Theme.getSize("sidebar_extruder_box").height : 0 + property var printerModel + Rectangle + { + color: UM.Theme.getColor("sidebar") + anchors.fill: parent + + Label //Build plate label. + { + text: catalog.i18nc("@label", "Build plate") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: UM.Theme.getSize("default_margin").width + } + + Label //Target temperature. + { + id: bedTargetTemperature + text: printerModel != null ? printerModel.targetBedTemperature + "°C" : "" + font: UM.Theme.getFont("small") + color: UM.Theme.getColor("text_inactive") + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.bottom: bedCurrentTemperature.bottom + + MouseArea //For tooltip. + { + id: bedTargetTemperatureTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: bedTargetTemperature.mapToItem(base, 0, -parent.height / 4).y}, + catalog.i18nc("@tooltip", "The target temperature of the heated bed. The bed will heat up or cool down towards this temperature. If this is 0, the bed heating is turned off.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + Label //Current temperature. + { + id: bedCurrentTemperature + text: printerModel != null ? printerModel.bedTemperature + "°C" : "" + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + anchors.right: bedTargetTemperature.left + anchors.top: parent.top + anchors.margins: UM.Theme.getSize("default_margin").width + + MouseArea //For tooltip. + { + id: bedTemperatureTooltipArea + hoverEnabled: true + anchors.fill: parent + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: bedCurrentTemperature.mapToItem(base, 0, -parent.height / 4).y}, + catalog.i18nc("@tooltip", "The current temperature of the heated bed.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } + Rectangle //Input field for pre-heat temperature. + { + id: preheatTemperatureControl + color: !enabled ? UM.Theme.getColor("setting_control_disabled") : showError ? UM.Theme.getColor("setting_validation_error_background") : UM.Theme.getColor("setting_validation_ok") + property var showError: + { + if(bedTemperature.properties.maximum_value != "None" && bedTemperature.properties.maximum_value < Math.floor(preheatTemperatureInput.text)) + { + return true; + } else + { + return false; + } + } + enabled: + { + if (printerModel == null) + { + return false; //Can't preheat if not connected. + } + if (!connectedPrinter.acceptsCommands) + { + return false; //Not allowed to do anything. + } + if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") + { + return false; //Printer is in a state where it can't react to pre-heating. + } + return true; + } + border.width: UM.Theme.getSize("default_lining").width + border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.bottom: parent.bottom + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + width: UM.Theme.getSize("setting_control").width + height: UM.Theme.getSize("setting_control").height + visible: printerModel != null ? printerModel.canPreHeatBed: true + Rectangle //Highlight of input field. + { + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_lining").width + color: UM.Theme.getColor("setting_control_highlight") + opacity: preheatTemperatureControl.hovered ? 1.0 : 0 + } + Label //Maximum temperature indication. + { + text: (bedTemperature.properties.maximum_value != "None" ? bedTemperature.properties.maximum_value : "") + "°C" + color: UM.Theme.getColor("setting_unit") + font: UM.Theme.getFont("default") + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.verticalCenter: parent.verticalCenter + } + MouseArea //Change cursor on hovering. + { + id: preheatTemperatureInputMouseArea + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.IBeamCursor + + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: preheatTemperatureInputMouseArea.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the bed to.") + ); + } + else + { + base.hideTooltip(); + } + } + } + TextInput + { + id: preheatTemperatureInput + font: UM.Theme.getFont("default") + color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") + selectByMouse: true + maximumLength: 10 + enabled: parent.enabled + validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + renderType: Text.NativeRendering + + Component.onCompleted: + { + if (!bedTemperature.properties.value) + { + text = ""; + } + if ((bedTemperature.resolve != "None" && bedTemperature.resolve) && (bedTemperature.stackLevels[0] != 0) && (bedTemperature.stackLevels[0] != 1)) + { + // We have a resolve function. Indicates that the setting is not settable per extruder and that + // we have to choose between the resolved value (default) and the global value + // (if user has explicitly set this). + text = bedTemperature.resolve; + } + else + { + text = bedTemperature.properties.value; + } + } + } + } + + UM.RecolorImage + { + id: preheatCountdownIcon + width: UM.Theme.getSize("save_button_specs_icons").width + height: UM.Theme.getSize("save_button_specs_icons").height + sourceSize.width: width + sourceSize.height: height + color: UM.Theme.getColor("text") + visible: preheatCountdown.visible + source: UM.Theme.getIcon("print_time") + anchors.right: preheatCountdown.left + anchors.rightMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) + anchors.verticalCenter: preheatCountdown.verticalCenter + } + + Timer + { + id: preheatUpdateTimer + interval: 100 //Update every 100ms. You want to update every 1s, but then you have one timer for the updating running out of sync with the actual date timer and you might skip seconds. + running: printerModel != null && printerModel.preheatBedRemainingTime != "" + repeat: true + onTriggered: update() + property var endTime: new Date() //Set initial endTime to be the current date, so that the endTime has initially already passed and the timer text becomes invisible if you were to update. + function update() + { + preheatCountdown.text = "" + if (printerModel != null) + { + preheatCountdown.text = connectedPrinter.preheatBedRemainingTime; + } + if (preheatCountdown.text == "") //Either time elapsed or not connected. + { + stop(); + } + } + } + Label + { + id: preheatCountdown + text: printerModel != null ? printerModel.preheatBedRemainingTime : "" + visible: text != "" //Has no direct effect, but just so that we can link visibility of clock icon to visibility of the countdown text. + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + anchors.right: preheatButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: preheatButton.verticalCenter + } + + Button //The pre-heat button. + { + id: preheatButton + height: UM.Theme.getSize("setting_control").height + visible: printerModel != null ? printerModel.canPreHeatBed: true + enabled: + { + if (!preheatTemperatureControl.enabled) + { + return false; //Not connected, not authenticated or printer is busy. + } + if (preheatUpdateTimer.running) + { + return true; //Can always cancel if the timer is running. + } + if (bedTemperature.properties.minimum_value != "None" && Math.floor(preheatTemperatureInput.text) < Math.floor(bedTemperature.properties.minimum_value)) + { + return false; //Target temperature too low. + } + if (bedTemperature.properties.maximum_value != "None" && Math.floor(preheatTemperatureInput.text) > Math.floor(bedTemperature.properties.maximum_value)) + { + return false; //Target temperature too high. + } + if (Math.floor(preheatTemperatureInput.text) == 0) + { + return false; //Setting the temperature to 0 is not allowed (since that cancels the pre-heating). + } + return true; //Preconditions are met. + } + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").width + style: ButtonStyle { + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("default_margin").width * 2) + border.color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_border"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_border"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_border"); + } + else + { + return UM.Theme.getColor("action_button_border"); + } + } + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered"); + } + else + { + return UM.Theme.getColor("action_button"); + } + } + Behavior on color + { + ColorAnimation + { + duration: 50 + } + } + + Label + { + id: actualLabel + anchors.centerIn: parent + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_text"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_text"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_text"); + } + else + { + return UM.Theme.getColor("action_button_text"); + } + } + font: UM.Theme.getFont("action_button") + text: preheatUpdateTimer.running ? catalog.i18nc("@button Cancel pre-heating", "Cancel") : catalog.i18nc("@button", "Pre-heat") + } + } + } + + onClicked: + { + if (!preheatUpdateTimer.running) + { + printerModel.preheatBed(preheatTemperatureInput.text, printerModel.preheatBedTimeout); + preheatUpdateTimer.start(); + preheatUpdateTimer.update(); //Update once before the first timer is triggered. + } + else + { + printerModel.cancelPreheatBed(); + preheatUpdateTimer.update(); + } + } + + onHoveredChanged: + { + if (hovered) + { + base.showTooltip( + base, + {x: 0, y: preheatButton.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip of pre-heat", "Heat the bed in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the bed to heat up when you're ready to print.") + ); + } + else + { + base.hideTooltip(); + } + } + } + } +} \ No newline at end of file From f987e6d977d8108ab6225b6d3a6df401d89a57b8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 21 Nov 2017 16:59:17 +0100 Subject: [PATCH 030/135] Functionality properties (canPause, canPreHeatBed, etc) are now in the Controller. It's actually up to the controller to say something about this, so this location makes more sense CL-541 --- cura/PrinterOutput/PrinterOutputController.py | 5 ++++- cura/PrinterOutput/PrinterOutputModel.py | 22 ++++++++++--------- resources/qml/PrinterOutput/HeatedBedBox.qml | 4 ++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 9f9a26a2a5..525c8db102 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -9,7 +9,10 @@ if MYPY: class PrinterOutputController: def __init__(self): - pass + self.can_pause = True + self.can_abort = True + self.can_pre_heat_bed = True + self.can_control_manually = True def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int): # TODO: implement diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 12c2b4fe58..97f5c69723 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -35,12 +35,6 @@ class PrinterOutputModel(QObject): self._printer_state = "unknown" - # Features of the printer; - self._can_pause = True - self._can_abort = True - self._can_pre_heat_bed = True - self._can_control_manually = True - @pyqtProperty(str, notify=keyChanged) def key(self): return self._key @@ -175,19 +169,27 @@ class PrinterOutputModel(QObject): # Does the printer support pre-heating the bed at all @pyqtProperty(bool, constant=True) def canPreHeatBed(self): - return self._can_pre_heat_bed + if self._controller: + return self._controller.can_pre_heat_bed + return False # Does the printer support pause at all @pyqtProperty(bool, constant=True) def canPause(self): - return self._can_pause + if self._controller: + return self.can_pause + return False # Does the printer support abort at all @pyqtProperty(bool, constant=True) def canAbort(self): - return self._can_abort + if self._controller: + return self.can_abort + return False # Does the printer support manual control at all @pyqtProperty(bool, constant=True) def canControlManually(self): - return self._can_control_manually + if self._controller: + return self.can_control_manually + return False diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index 6ff48df6a2..de34fe5943 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -229,6 +229,10 @@ Item property var endTime: new Date() //Set initial endTime to be the current date, so that the endTime has initially already passed and the timer text becomes invisible if you were to update. function update() { + if(printerModel != null && !printerModel.canPreHeatBed) + { + return // Nothing to do, printer cant preheat at all! + } preheatCountdown.text = "" if (printerModel != null) { From 7465a6551a7a0f8331237dc2f1bd27c1e9d7c306 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 22 Nov 2017 11:59:07 +0100 Subject: [PATCH 031/135] Setup the authentication stuff for LegacyUM3 CL-541 --- .../NetworkedPrinterOutputDevice.py | 39 ++- cura/PrinterOutputDevice.py | 2 + .../LegacyUM3OutputDevice.py | 249 +++++++++++++++++- 3 files changed, 284 insertions(+), 6 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index d2886328de..b9bd27c129 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -8,9 +8,19 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, py from time import time from typing import Callable, Any +from enum import IntEnum + + +class AuthState(IntEnum): + NotAuthenticated = 1 + AuthenticationRequested = 2 + Authenticated = 3 + AuthenticationDenied = 4 + AuthenticationReceived = 5 class NetworkedPrinterOutputDevice(PrinterOutputDevice): + authenticationStateChanged = pyqtSignal() def __init__(self, device_id, address: str, properties, parent = None): super().__init__(device_id = device_id, parent = parent) self._manager = None @@ -27,6 +37,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()) self._onFinishedCallbacks = {} + self._authentication_state = AuthState.NotAuthenticated + + def setAuthenticationState(self, authentication_state): + if self._authentication_state != authentication_state: + self._authentication_state = authentication_state + self.authenticationStateChanged.emit() + + @pyqtProperty(int, notify=authenticationStateChanged) + def authenticationState(self): + return self._authentication_state def _update(self): if self._last_response_time: @@ -81,23 +101,30 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_request_time = time() pass - def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable): + def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() + request = self._createEmptyRequest(target) self._last_request_time = time() - pass + reply = self._manager.post(request, data) + if onProgress is not None: + reply.uploadProgress.connect(onProgress) + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + + def _onAuthenticationRequired(self, reply, authenticator): + Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString())) def _createNetworkManager(self): Logger.log("d", "Creating network manager") if self._manager: self._manager.finished.disconnect(self.__handleOnFinished) #self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) - #self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) + self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) self._manager = QNetworkAccessManager() self._manager.finished.connect(self.__handleOnFinished) self._last_manager_create_time = time() - #self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + self._manager.authenticationRequired.connect(self._onAuthenticationRequired) #self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes def __handleOnFinished(self, reply: QNetworkReply): @@ -107,7 +134,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() - self.setConnectionState(ConnectionState.connected) + if self._connection_state == ConnectionState.connecting: + self.setConnectionState(ConnectionState.connected) + try: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply) except Exception: diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 56ac318f20..a170037311 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -135,11 +135,13 @@ class PrinterOutputDevice(QObject, OutputDevice): ## Attempt to establish connection def connect(self): + self.setConnectionState(ConnectionState.connecting) self._update_timer.start() ## Attempt to close the connection def close(self): self._update_timer.stop() + self.setConnectionState(ConnectionState.closed) ## Ensure that close gets called when object is destroyed def __del__(self): diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 60409ec729..cb9959ec69 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -1,28 +1,256 @@ -from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice +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 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 import json +import os # To get the username + +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._not_authenticated_message = None + + 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._requestAuthentication) + 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._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._authentication_timer.stop() + + ## Send all material profiles to the printer. + def sendMaterialProfiles(self): + # TODO + pass + 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._key) + 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: + 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._key) + def _onGetPrintJobFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) @@ -104,3 +332,22 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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 + + ## Convenience function to get the username from the OS. + # The code was copied from the getpass module, as we try to use as little dependencies as possible. + def _getUserName(self): + for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): + user = os.environ.get(name) + if user: + return user + return "Unknown User" # Couldn't find out username. \ No newline at end of file From 96d5c7152b2a3a742b42d2062dd97710e182da90 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 10:31:39 +0100 Subject: [PATCH 032/135] Added sending material profiles to LegacyUM3 CL-541 --- .../NetworkedPrinterOutputDevice.py | 57 ++++++++++++++++--- .../LegacyUM3OutputDevice.py | 25 ++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index b9bd27c129..395771b833 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -7,7 +7,7 @@ from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetwork from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl from time import time -from typing import Callable, Any +from typing import Callable, Any, Optional from enum import IntEnum @@ -39,6 +39,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._onFinishedCallbacks = {} self._authentication_state = AuthState.NotAuthenticated + self._cached_multiparts = {} + def setAuthenticationState(self, authentication_state): if self._authentication_state != authentication_state: self._authentication_state = authentication_state @@ -79,29 +81,35 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - def _put(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None]): + def _clearCachedMultiPart(self, reply): + if id(reply) in self._cached_multiparts: + del self._cached_multiparts[id(reply)] + + def _put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.put(request, data.encode()) - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + if onFinished is not None: + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _get(self, target: str, onFinished: Callable[[Any, QNetworkReply], None]): + def _get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.get(request) - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + if onFinished is not None: + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _delete(self, target: str, onFinished: Callable[[Any, QNetworkReply], None]): + def _delete(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): if self._manager is None: self._createNetworkManager() self._last_request_time = time() pass - def _post(self, target: str, data: str, onFinished: Callable[[Any, QNetworkReply], None], onProgress: Callable = None): + def _post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -109,7 +117,31 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.post(request, data) if onProgress is not None: reply.uploadProgress.connect(onProgress) - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + if onFinished is not None: + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + + def _postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + if self._manager is None: + self._createNetworkManager() + request = self._createEmptyRequest(target) + + multi_post_part = QHttpMultiPart() + post_part = QHttpPart() + post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) + post_part.setBody(body_data) + multi_post_part.append(post_part) + + self._last_request_time = time() + + reply = self._manager.post(request, multi_post_part) + + # Due to garbage collection on python doing some weird stuff, we need to keep hold of a reference + self._cached_multiparts[id(reply)] = (post_part, multi_post_part, reply) + + if onProgress is not None: + reply.uploadProgress.connect(onProgress) + if onFinished is not None: + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished def _onAuthenticationRequired(self, reply, authenticator): Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString())) @@ -128,6 +160,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): #self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes def __handleOnFinished(self, reply: QNetworkReply): + # Due to garbage collection, we need to cache certain bits of post operations. + # As we don't want to keep them around forever, delete them if we get a reply. + if reply.operation() == QNetworkAccessManager.PostOperation: + self._clearCachedMultiPart(reply) + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None: # No status code means it never even reached remote. return @@ -137,8 +174,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) + callback_key = reply.url().toString() + str(reply.operation()) try: - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())](reply) + if callback_key in self._onFinishedCallbacks: + self._onFinishedCallbacks[callback_key](reply) except Exception: Logger.logException("w", "something went wrong with callback") diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index cb9959ec69..1cd5a19fe4 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -3,6 +3,8 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel +from cura.Settings.ContainerManager import ContainerManager + from UM.Logger import Logger from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Application import Application @@ -97,6 +99,29 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): ## 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 + # TODO pass From 8b8d67b3a83bac74e366a8afaacc9ddfc0a1e41a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 13:37:59 +0100 Subject: [PATCH 033/135] LegacyUM3 now handles warnings & errors again CL-541 --- cura/PrinterOutput/MaterialOutputModel.py | 9 +- .../NetworkedPrinterOutputDevice.py | 4 + cura/PrinterOutput/PrinterOutputModel.py | 1 + cura/PrinterOutputDevice.py | 17 +- .../LegacyUM3OutputDevice.py | 146 +++++++++++++++++- 5 files changed, 166 insertions(+), 11 deletions(-) diff --git a/cura/PrinterOutput/MaterialOutputModel.py b/cura/PrinterOutput/MaterialOutputModel.py index 0471b85db8..64ebd3c94c 100644 --- a/cura/PrinterOutput/MaterialOutputModel.py +++ b/cura/PrinterOutput/MaterialOutputModel.py @@ -5,12 +5,13 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot class MaterialOutputModel(QObject): - def __init__(self, guid, type, color, brand, parent = None): + def __init__(self, guid, type, color, brand, name, parent = None): super().__init__(parent) self._guid = guid self._type = type self._color = color self._brand = brand + self._name = name @pyqtProperty(str, constant = True) def guid(self): @@ -26,4 +27,8 @@ class MaterialOutputModel(QObject): @pyqtProperty(str, constant=True) def color(self): - return self._color \ No newline at end of file + return self._color + + @pyqtProperty(str, constant=True) + def name(self): + return self._name \ No newline at end of file diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 395771b833..97960db1f3 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -21,6 +21,7 @@ class AuthState(IntEnum): class NetworkedPrinterOutputDevice(PrinterOutputDevice): authenticationStateChanged = pyqtSignal() + def __init__(self, device_id, address: str, properties, parent = None): super().__init__(device_id = device_id, parent = parent) self._manager = None @@ -41,6 +42,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._cached_multiparts = {} + def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): + raise NotImplementedError("requestWrite needs to be implemented") + def setAuthenticationState(self, authentication_state): if self._authentication_state != authentication_state: self._authentication_state = authentication_state diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 97f5c69723..23423609f7 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -11,6 +11,7 @@ MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController + from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel class PrinterOutputModel(QObject): diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index a170037311..9744f352fd 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -5,13 +5,19 @@ from UM.i18n import i18nCatalog from UM.OutputDevice.OutputDevice import OutputDevice from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl from PyQt5.QtQml import QQmlComponent, QQmlContext -from enum import IntEnum # For the connection state tracking. + from UM.Logger import Logger from UM.Signal import signalemitter from UM.Application import Application import os +from enum import IntEnum # For the connection state tracking. +from typing import List, Optional + +MYPY = False +if MYPY: + from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel i18n_catalog = i18nCatalog("cura") @@ -32,7 +38,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) - self._printers = [] + self._printers = [] # type: List[PrinterOutputModel] self._monitor_view_qml_path = "" self._monitor_component = None @@ -62,20 +68,19 @@ class PrinterOutputDevice(QObject, OutputDevice): def _update(self): pass - def _getPrinterByKey(self, key): + def _getPrinterByKey(self, key) -> Optional["PrinterOutputModel"]: for printer in self._printers: if printer.key == key: return printer return None - def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): raise NotImplementedError("requestWrite needs to be implemented") @pyqtProperty(QObject, notify = printersChanged) - def activePrinter(self): + def activePrinter(self) -> Optional["PrinterOutputModel"]: if len(self._printers): - return self._printers[0] return None diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 1cd5a19fe4..37d02013b9 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -4,6 +4,7 @@ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from cura.Settings.ContainerManager import ContainerManager +from cura.Settings.ExtruderManager import ExtruderManager from UM.Logger import Logger from UM.Settings.ContainerRegistry import ContainerRegistry @@ -13,6 +14,7 @@ from UM.Message import Message from PyQt5.QtNetwork import QNetworkRequest from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QMessageBox import json import os # To get the username @@ -122,8 +124,144 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # NotImplementedError. We can simply ignore these. pass - # TODO - 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.printerState 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().showPrintMonitor.emit(True) + self.writeStarted.emit(self) + + gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", None) + if gcode is None: + # 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): + # TODO: Implement + Logger.log("i", "Sending print job to printer.") + return + + def _messageBoxCallback(self, button): + def delayedCallback(): + if button == QMessageBox.Yes: + self._startPrint() + else: + Application.getInstance().showPrintMonitor.emit(False) + # 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(): @@ -339,13 +477,15 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + brand=brand, color=color, name = name) extruder.updateActiveMaterial(material) try: From f03a9787817674faea1f0601acfedcbed2abcf00 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 14:19:54 +0100 Subject: [PATCH 034/135] Sending & compressing g-codes re-added to LegacyUM3 CL-541 --- .../LegacyUM3OutputDevice.py | 107 +++++++++++++++++- 1 file changed, 102 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 37d02013b9..e9963c678b 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -13,11 +13,15 @@ from UM.i18n import i18nCatalog from UM.Message import Message from PyQt5.QtNetwork import QNetworkRequest -from PyQt5.QtCore import QTimer +from PyQt5.QtCore import QTimer, QCoreApplication from PyQt5.QtWidgets import QMessageBox +from time import time + import json import os # To get the username +import gzip + i18n_catalog = i18nCatalog("cura") @@ -56,6 +60,10 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_failed_message = None self._not_authenticated_message = None + self._sending_gcode = False + self._compressing_gcode = False + self._gcode = [] + 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"), @@ -96,7 +104,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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. @@ -141,8 +150,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): Application.getInstance().showPrintMonitor.emit(True) self.writeStarted.emit(self) - gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", None) - if gcode is None: + self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", []) + if not self._gcode: # Unable to find g-code. Nothing to send return @@ -192,10 +201,98 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._startPrint() def _startPrint(self): - # TODO: Implement 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().showPrintMonitor.emit(False) + + def _onPostPrintJobFinished(self, reply): + self._progress_message.hide() + self._sending_gcode = False + + def __compressDataAndNotifyQt(self, 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. + # If we don't do this, the device might trigger a timeout. + self._last_response_time = time() + return compressed_data + + 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 _compressGCode(self): + self._compressing_gcode = True + + ## Mash the data into single string + max_chars_per_line = 1024 * 1024 / 4 # 1/4 MB per line. + byte_array_file_data = b"" + batched_line = "" + + for line in self._gcode: + if not self._compressing_gcode: + self._progress_message.hide() + # Stop trying to zip / send as abort was called. + return + 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 += self.__compressDataAndNotifyQt(batched_line) + batched_line = "" + + # Don't miss the last batch (If any) + if batched_line: + byte_array_file_data += self.__compressDataAndNotifyQt(batched_line) + + self._compressing_gcode = False + return byte_array_file_data + def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: From d0c7352be6b9b42a1ce14c415aab9c1b3d87bae3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 16:16:57 +0100 Subject: [PATCH 035/135] Added missing authentication_succeeded_message attribute to constructor CL-541 --- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index e9963c678b..7c76811fd2 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -58,6 +58,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # 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._sending_gcode = False From 0b91112d72fb63e3c0f3de92392a7e3d77b1e12f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 16:43:52 +0100 Subject: [PATCH 036/135] Fixed postForm Setting the type of the request to json messed up the multi-part stuff. CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 97960db1f3..dcd6b5ca70 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -78,6 +78,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): return True + def _createEmptyFormRequest(self, target): + url = QUrl("http://" + self._address + self._api_prefix + target) + request = QNetworkRequest(url) + request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) + return request + def _createEmptyRequest(self, target): url = QUrl("http://" + self._address + self._api_prefix + target) request = QNetworkRequest(url) @@ -127,9 +133,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() - request = self._createEmptyRequest(target) + request = self._createEmptyFormRequest(target) - multi_post_part = QHttpMultiPart() + multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) post_part.setBody(body_data) From 1c2c4d4163e4546a7a57c983fa2f64ce0f0e8ea2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 17:07:24 +0100 Subject: [PATCH 037/135] Added property to indicate if output device accepts commands Instead of how this was previously done, it's now tied to the auth state. CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 3 +++ cura/PrinterOutputDevice.py | 13 +++++++++++++ plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index dcd6b5ca70..f8d2ec66e2 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -1,3 +1,6 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from UM.Application import Application from UM.Logger import Logger diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 9744f352fd..5b747d19bf 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -34,6 +34,7 @@ i18n_catalog = i18nCatalog("cura") class PrinterOutputDevice(QObject, OutputDevice): printersChanged = pyqtSignal() connectionStateChanged = pyqtSignal(str) + acceptsCommandsChanged = pyqtSignal() def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) @@ -49,6 +50,7 @@ class PrinterOutputDevice(QObject, OutputDevice): self._control_item = None self._qml_context = None + self._accepts_commands = False self._update_timer = QTimer() self._update_timer.setInterval(2000) # TODO; Add preference for update interval @@ -152,6 +154,17 @@ class PrinterOutputDevice(QObject, OutputDevice): def __del__(self): self.close() + @pyqtProperty(bool, notify=acceptsCommandsChanged) + def acceptsCommands(self): + return self._accepts_commands + + ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands + def setAcceptsCommands(self, accepts_commands): + if self._accepts_commands != accepts_commands: + self._accepts_commands = accepts_commands + + self.acceptsCommandsChanged.emit() + ## The current processing state of the backend. class ConnectionState(IntEnum): diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 7c76811fd2..67db519c9e 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -65,6 +65,15 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._compressing_gcode = False self._gcode = [] + self.authenticationStateChanged.connect(self._onAuthenticationStateChanged) + + 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"), From 4597bb09ed2bc88eec12dfb037fe98078b35780c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 23 Nov 2017 17:08:22 +0100 Subject: [PATCH 038/135] Added (short) description & priority to legacy output device. CL-541 --- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 67db519c9e..b7736d675b 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -67,6 +67,13 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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") + def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. if self._authentication_state == AuthState.Authenticated: From c523a6ddf6f8c88071eae51b2c4d30e46e01433e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 24 Nov 2017 09:22:50 +0100 Subject: [PATCH 039/135] Progress is now shown for LegacyPrinter while printing CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 1 - .../LegacyUM3OutputDevice.py | 1 + .../UM3NetworkPrinting/UM3InfoComponents.qml | 8 +- .../UM3OutputDevicePlugin.py | 1 - resources/qml/MonitorButton.qml | 74 +++++++++++++++---- 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 23423609f7..97f5c69723 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -11,7 +11,6 @@ MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController - from cura.PrinterOutput.ExtruderOuputModel import ExtruderOutputModel class PrinterOutputModel(QObject): diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index b7736d675b..67b2032e6a 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -568,6 +568,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): if not self._printers: self._printers = [PrinterOutputModel(output_controller=None, number_of_extruders=self._number_of_extruders)] + self.printersChanged.emit() # LegacyUM3 always has a single printer. printer = self._printers[0] diff --git a/plugins/UM3NetworkPrinting/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/UM3InfoComponents.qml index d0c95e1524..939c6bcb39 100644 --- a/plugins/UM3NetworkPrinting/UM3InfoComponents.qml +++ b/plugins/UM3NetworkPrinting/UM3InfoComponents.qml @@ -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;*/ } } } diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 09bff8e7b8..aecbc1717c 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -84,7 +84,6 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): def _onDeviceConnectionStateChanged(self, key): if key not in self._discovered_devices: return - print("STATE CHANGED", key) if self._discovered_devices[key].isConnected(): self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key]) else: diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index 29b00f50e6..07a9e1913b 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -17,16 +17,39 @@ Item property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands - property real progress: printerConnected ? Cura.MachineManager.printerOutputDevices[0].progress : 0 + property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null + property var activePrintJob: activePrinter ? activePrinter.activePrintJob: null + property real progress: + { + if(!printerConnected) + { + return 0 + } + if(activePrinter == null) + { + return 0 + } + if(activePrintJob == null) + { + return 0 + } + if(activePrintJob.timeTotal == 0) + { + return 0 // Prevent devision by 0 + } + return activePrintJob.timeElapsed / activePrintJob.timeTotal * 100 + } + property int backendState: UM.Backend.state property bool showProgress: { // determine if we need to show the progress bar + percentage - if(!printerConnected || !printerAcceptsCommands) { + if(activePrintJob == null) + { return false; } - switch(Cura.MachineManager.printerOutputDevices[0].jobState) + switch(base.activePrintJob.state) { case "printing": case "paused": @@ -50,7 +73,7 @@ Item if(!printerConnected || !printerAcceptsCommands) return UM.Theme.getColor("text"); - switch(Cura.MachineManager.printerOutputDevices[0].printerState) + switch(activePrinter.printerState) { case "maintenance": return UM.Theme.getColor("status_busy"); @@ -58,7 +81,7 @@ Item return UM.Theme.getColor("status_stopped"); } - switch(Cura.MachineManager.printerOutputDevices[0].jobState) + switch(base.activePrintJob.state) { case "printing": case "pre_print": @@ -85,17 +108,27 @@ Item property string statusText: { if(!printerConnected) + { return catalog.i18nc("@label:MonitorStatus", "Not connected to a printer"); + } if(!printerAcceptsCommands) + { return catalog.i18nc("@label:MonitorStatus", "Printer does not accept commands"); + } var printerOutputDevice = Cura.MachineManager.printerOutputDevices[0] - if(printerOutputDevice.printerState == "maintenance") + if(activePrinter.printerState == "maintenance") { return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer"); } - switch(printerOutputDevice.jobState) + + if(base.activePrintJob == null) + { + return " " + } + + switch(base.activePrintJob.state) { case "offline": return catalog.i18nc("@label:MonitorStatus", "Lost connection with the printer"); @@ -163,7 +196,11 @@ Item { return false; } - switch(Cura.MachineManager.printerOutputDevices[0].jobState) + if(base.activePrintJob == null) + { + return false + } + switch(base.activePrintJob.state) { case "pausing": case "resuming": @@ -185,7 +222,8 @@ Item anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width; } - Row { + Row + { id: buttonsRow height: abortButton.height anchors.top: progressBar.bottom @@ -194,17 +232,21 @@ Item anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width spacing: UM.Theme.getSize("default_margin").width - Row { + Row + { id: additionalComponentsRow spacing: UM.Theme.getSize("default_margin").width } - Connections { + Connections + { target: Printer onAdditionalComponentsChanged: { - if(areaId == "monitorButtons") { - for (var component in CuraApplication.additionalComponents["monitorButtons"]) { + if(areaId == "monitorButtons") + { + for (var component in CuraApplication.additionalComponents["monitorButtons"]) + { CuraApplication.additionalComponents["monitorButtons"][component].parent = additionalComponentsRow } } @@ -220,7 +262,7 @@ Item property bool userClicked: false property string lastJobState: "" - visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].canPause + visible: printerConnected && activePrinter.canPause enabled: (!userClicked) && printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands && (["paused", "printing"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0) @@ -261,8 +303,8 @@ Item { id: abortButton - visible: printerConnected && Cura.MachineManager.printerOutputDevices[0].canAbort - enabled: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands && + visible: printerConnected && activePrinter.canAbort + enabled: printerConnected && activePrinter.acceptsCommands && (["paused", "printing", "pre_print"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0) height: UM.Theme.getSize("save_button_save_to_button").height From 9d7cd726915e563c169dcd9d68e88e03f35c2a13 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 24 Nov 2017 09:38:10 +0100 Subject: [PATCH 040/135] JobData is now shown in monitor screen again CL-541 --- resources/qml/PrintMonitor.qml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index f95d829306..172adc21c1 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -16,6 +16,7 @@ Column id: printMonitor property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null property var activePrinter: connectedDevice != null ? connectedDevice.activePrinter : null + property var activePrintJob: activePrinter != null ? activePrinter.activePrintJob: null Cura.ExtrudersModel { @@ -438,20 +439,33 @@ Column { sourceComponent: monitorItem property string label: catalog.i18nc("@label", "Job Name") - property string value: connectedPrinter != null ? connectedPrinter.jobName : "" + property string value: activePrintJob != null ? activePrintJob.name : "" } + Loader { sourceComponent: monitorItem property string label: catalog.i18nc("@label", "Printing Time") - property string value: connectedPrinter != null ? getPrettyTime(connectedPrinter.timeTotal) : "" + property string value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : "" } + Loader { sourceComponent: monitorItem property string label: catalog.i18nc("@label", "Estimated time left") - property string value: connectedPrinter != null ? getPrettyTime(connectedPrinter.timeTotal - connectedPrinter.timeElapsed) : "" - visible: connectedPrinter != null && (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused") + property string value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal - activePrintJob.timeElapsed) : "" + visible: + { + if(activePrintJob == null) + { + return false + } + + return (activePrintJob.state == "printing" || + activePrintJob.state == "resuming" || + activePrintJob.state == "pausing" || + activePrintJob.state == "paused") + } } Component @@ -485,6 +499,7 @@ Column } } } + Component { id: monitorSection From 57406100ef5355d6093514e931a472a89716dc5f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 24 Nov 2017 10:55:24 +0100 Subject: [PATCH 041/135] Fixed status icon in monitor tab CL-541 --- resources/qml/PrintMonitor.qml | 6 +++--- resources/qml/Topbar.qml | 13 +++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 172adc21c1..5b6f96dfc1 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -100,15 +100,15 @@ Column visible: connectedPrinter != null ? connectedPrinter.canControlManually : false enabled: { - if (connectedPrinter == null) + if (connectedPrinter == null || activePrintJob == null) { - return false; //Can't control the printer if not connected. + return false; //Can't control the printer if not connected or if there is no print job. } if (!connectedPrinter.acceptsCommands) { return false; //Not allowed to do anything. } - if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") + if (activePrintJob.state == "printing" || activePrintJob.state == "resuming" || activePrintJob.state == "pausing" || activePrintJob.state == "error" || activePrintJob.state == "offline") { return false; //Printer is in a state where it can't react to manual control } diff --git a/resources/qml/Topbar.qml b/resources/qml/Topbar.qml index 6085c6fe7e..63d0981830 100644 --- a/resources/qml/Topbar.qml +++ b/resources/qml/Topbar.qml @@ -124,13 +124,22 @@ Rectangle { return UM.Theme.getIcon("tab_status_unknown"); } - if (Cura.MachineManager.printerOutputDevices[0].printerState == "maintenance") { return UM.Theme.getIcon("tab_status_busy"); } - switch (Cura.MachineManager.printerOutputDevices[0].jobState) + if(Cura.MachineManager.printerOutputDevices[0].activePrinter == null) + { + return UM.Theme.getIcon("tab_status_connected") + } + + if(Cura.MachineManager.printerOutputDevices[0].activePrinter.activePrintJob == null) + { + return UM.Theme.getIcon("tab_status_connected") + } + + switch (Cura.MachineManager.printerOutputDevices[0].activePrinter.activePrintJob.state) { case "printing": case "pre_print": From 57de0286081477ff77dd00f20a131237d0df4993 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 24 Nov 2017 11:26:30 +0100 Subject: [PATCH 042/135] re-implemented abort & pause for legacy um3 CL-541 --- .../NetworkedPrinterOutputDevice.py | 10 +++---- cura/PrinterOutput/PrintJobOutputModel.py | 3 +- cura/PrinterOutput/PrinterOutputController.py | 7 +++-- cura/PrinterOutput/PrinterOutputModel.py | 6 ++-- .../ClusterUM3OutputDevice.py | 4 +-- .../LegacyUM3OutputDevice.py | 28 +++++++++++-------- .../LegacyUM3PrinterOutputController.py | 13 +++++++++ resources/qml/MonitorButton.qml | 28 ++++++++++--------- 8 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index f8d2ec66e2..58c82b6c38 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -98,7 +98,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if id(reply) in self._cached_multiparts: del self._cached_multiparts[id(reply)] - def _put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): + def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -107,7 +107,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): + def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -116,13 +116,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _delete(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): + def delete(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): if self._manager is None: self._createNetworkManager() self._last_request_time = time() pass - def _post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -133,7 +133,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def _postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() request = self._createEmptyFormRequest(target) diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 7c38782788..00641ab89a 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -1,7 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot MYPY = False if MYPY: from cura.PrinterOutput.PrinterOutputController import PrinterOutputController @@ -80,5 +80,6 @@ class PrintJobOutputModel(QObject): self._state = new_state self.stateChanged.emit() + @pyqtSlot(str) def setState(self, state): self._output_controller.setJobState(self, state) \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 525c8db102..982c41f293 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -1,4 +1,5 @@ - +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. MYPY = False if MYPY: @@ -8,11 +9,13 @@ if MYPY: class PrinterOutputController: - def __init__(self): + def __init__(self, output_device): self.can_pause = True self.can_abort = True self.can_pre_heat_bed = True self.can_control_manually = True + self._output_device = output_device + def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int): # TODO: implement diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 97f5c69723..d4b9d9c99a 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -177,19 +177,19 @@ class PrinterOutputModel(QObject): @pyqtProperty(bool, constant=True) def canPause(self): if self._controller: - return self.can_pause + return self._controller.can_pause return False # Does the printer support abort at all @pyqtProperty(bool, constant=True) def canAbort(self): if self._controller: - return self.can_abort + return self._controller.can_abort return False # Does the printer support manual control at all @pyqtProperty(bool, constant=True) def canControlManually(self): if self._controller: - return self.can_control_manually + return self._controller.can_control_manually return False diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 8f9a92384f..91acdc28af 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -21,8 +21,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def _update(self): if not super()._update(): return - self._get("printers/", onFinished=self._onGetPrintersDataFinished) - self._get("print_jobs/", onFinished=self._onGetPrintJobsFinished) + self.get("printers/", onFinished=self._onGetPrintersDataFinished) + self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 67b2032e6a..de0a8d6eff 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -16,6 +16,8 @@ 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 @@ -74,6 +76,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self.setIconName("print") + self._output_controller = LegacyUM3PrinterOutputController(self) + def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. if self._authentication_state == AuthState.Authenticated: @@ -143,7 +147,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + 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 @@ -241,8 +245,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, + onFinished=self._onPostPrintJobFinished) return @@ -392,8 +396,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + self.get("printer", onFinished=self._onGetPrinterDataFinished) + self.get("print_job", onFinished=self._onGetPrintJobFinished) def _resetAuthenticationRequestedMessage(self): if self._authentication_requested_message: @@ -415,7 +419,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted) def _onVerifyAuthenticationCompleted(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) @@ -438,7 +442,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + 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(): @@ -511,10 +515,10 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_key = None self._authentication_id = None - self._post("auth/request", - json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), + self.post("auth/request", + json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode(), - onFinished=self._onRequestAuthenticationFinished) + onFinished=self._onRequestAuthenticationFinished) self.setAuthenticationState(AuthState.AuthenticationRequested) @@ -542,7 +546,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): Logger.log("w", "Received an invalid print job state message: Not valid JSON.") return if printer.activePrintJob is None: - print_job = PrintJobOutputModel(output_controller=None) + print_job = PrintJobOutputModel(output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob @@ -567,7 +571,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): return if not self._printers: - self._printers = [PrinterOutputModel(output_controller=None, number_of_extruders=self._number_of_extruders)] + self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders)] self.printersChanged.emit() # LegacyUM3 always has a single printer. diff --git a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py new file mode 100644 index 0000000000..e303d237ce --- /dev/null +++ b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py @@ -0,0 +1,13 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from cura.PrinterOutput.PrinterOutputController import PrinterOutputController + + +class LegacyUM3PrinterOutputController(PrinterOutputController): + def __init__(self, output_device): + super().__init__(output_device) + + def setJobState(self, job: "PrintJobOutputModel", state: str): + data = "{\"target\": \"%s\"}" % state + self._output_device.put("print_job/state", data, onFinished=None) diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index 07a9e1913b..6166f9b62f 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -263,18 +263,17 @@ Item property string lastJobState: "" visible: printerConnected && activePrinter.canPause - enabled: (!userClicked) && printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands && - (["paused", "printing"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0) + enabled: (!userClicked) && printerConnected && printerAcceptsCommands && activePrintJob != null && + (["paused", "printing"].indexOf(activePrintJob.state) >= 0) text: { var result = ""; - if (!printerConnected) + if (!printerConnected || activePrintJob == null) { return ""; } - var jobState = Cura.MachineManager.printerOutputDevices[0].jobState; - if (jobState == "paused") + if (activePrintJob.state == "paused") { return catalog.i18nc("@label:", "Resume"); } @@ -285,14 +284,17 @@ Item } onClicked: { - var current_job_state = Cura.MachineManager.printerOutputDevices[0].jobState - if(current_job_state == "paused") + if(activePrintJob == null) { - Cura.MachineManager.printerOutputDevices[0].setJobState("print"); + return // Do nothing! } - else if(current_job_state == "printing") + if(activePrintJob.state == "paused") { - Cura.MachineManager.printerOutputDevices[0].setJobState("pause"); + activePrintJob.setState("print"); + } + else if(activePrintJob.state == "printing") + { + activePrintJob.setState("pause"); } } @@ -304,8 +306,8 @@ Item id: abortButton visible: printerConnected && activePrinter.canAbort - enabled: printerConnected && activePrinter.acceptsCommands && - (["paused", "printing", "pre_print"].indexOf(Cura.MachineManager.printerOutputDevices[0].jobState) >= 0) + enabled: printerConnected && printerAcceptsCommands && activePrintJob != null && + (["paused", "printing", "pre_print"].indexOf(activePrintJob.state) >= 0) height: UM.Theme.getSize("save_button_save_to_button").height @@ -324,7 +326,7 @@ Item text: catalog.i18nc("@label", "Are you sure you want to abort the print?") standardButtons: StandardButton.Yes | StandardButton.No Component.onCompleted: visible = false - onYes: Cura.MachineManager.printerOutputDevices[0].setJobState("abort") + onYes: activePrintJob.setState("abort") } } } From 5036eccd32ffff186b8a11ac2bd4cc773845226c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 10:15:28 +0100 Subject: [PATCH 043/135] Moved multiple components to their own files CL-541 --- resources/qml/PrintMonitor.qml | 506 +----------------- resources/qml/PrinterOutput/HeatedBedBox.qml | 2 +- .../PrinterOutput/ManualPrinterControl.qml | 442 +++++++++++++++ resources/qml/PrinterOutput/MonitorItem.qml | 44 ++ .../qml/PrinterOutput/MonitorSection.qml | 33 ++ 5 files changed, 542 insertions(+), 485 deletions(-) create mode 100644 resources/qml/PrinterOutput/ManualPrinterControl.qml create mode 100644 resources/qml/PrinterOutput/MonitorItem.qml create mode 100644 resources/qml/PrinterOutput/MonitorSection.qml diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 5b6f96dfc1..830093cd2b 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -95,365 +95,38 @@ Column watchedProperties: ["value"] } - Column + ManualPrinterControl { - visible: connectedPrinter != null ? connectedPrinter.canControlManually : false - enabled: - { - if (connectedPrinter == null || activePrintJob == null) - { - return false; //Can't control the printer if not connected or if there is no print job. - } - if (!connectedPrinter.acceptsCommands) - { - return false; //Not allowed to do anything. - } - if (activePrintJob.state == "printing" || activePrintJob.state == "resuming" || activePrintJob.state == "pausing" || activePrintJob.state == "error" || activePrintJob.state == "offline") - { - return false; //Printer is in a state where it can't react to manual control - } - return true; - } - - Loader - { - sourceComponent: monitorSection - property string label: catalog.i18nc("@label", "Printer control") - } - - Row - { - width: base.width - 2 * UM.Theme.getSize("default_margin").width - height: childrenRect.height + UM.Theme.getSize("default_margin").width - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - - spacing: UM.Theme.getSize("default_margin").width - - Label - { - text: catalog.i18nc("@label", "Jog Position") - color: UM.Theme.getColor("setting_control_text") - font: UM.Theme.getFont("default") - - width: Math.floor(parent.width * 0.4) - UM.Theme.getSize("default_margin").width - height: UM.Theme.getSize("setting_control").height - verticalAlignment: Text.AlignVCenter - } - - GridLayout - { - columns: 3 - rows: 4 - rowSpacing: UM.Theme.getSize("default_lining").width - columnSpacing: UM.Theme.getSize("default_lining").height - - Label - { - text: catalog.i18nc("@label", "X/Y") - color: UM.Theme.getColor("setting_control_text") - font: UM.Theme.getFont("default") - width: height - height: UM.Theme.getSize("setting_control").height - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - Layout.row: 1 - Layout.column: 2 - Layout.preferredWidth: width - Layout.preferredHeight: height - } - - Button - { - Layout.row: 2 - Layout.column: 2 - Layout.preferredWidth: width - Layout.preferredHeight: height - iconSource: UM.Theme.getIcon("arrow_top"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.moveHead(0, distancesRow.currentDistance, 0) - } - } - - Button - { - Layout.row: 3 - Layout.column: 1 - Layout.preferredWidth: width - Layout.preferredHeight: height - iconSource: UM.Theme.getIcon("arrow_left"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.moveHead(-distancesRow.currentDistance, 0, 0) - } - } - - Button - { - Layout.row: 3 - Layout.column: 3 - Layout.preferredWidth: width - Layout.preferredHeight: height - iconSource: UM.Theme.getIcon("arrow_right"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.moveHead(distancesRow.currentDistance, 0, 0) - } - } - - Button - { - Layout.row: 4 - Layout.column: 2 - Layout.preferredWidth: width - Layout.preferredHeight: height - iconSource: UM.Theme.getIcon("arrow_bottom"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.moveHead(0, -distancesRow.currentDistance, 0) - } - } - - Button - { - Layout.row: 3 - Layout.column: 2 - Layout.preferredWidth: width - Layout.preferredHeight: height - iconSource: UM.Theme.getIcon("home"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.homeHead() - } - } - } - - - Column - { - spacing: UM.Theme.getSize("default_lining").height - - Label - { - text: catalog.i18nc("@label", "Z") - color: UM.Theme.getColor("setting_control_text") - font: UM.Theme.getFont("default") - width: UM.Theme.getSize("section").height - height: UM.Theme.getSize("setting_control").height - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - Button - { - iconSource: UM.Theme.getIcon("arrow_top"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.moveHead(0, 0, distancesRow.currentDistance) - } - } - - Button - { - iconSource: UM.Theme.getIcon("home"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.homeBed() - } - } - - Button - { - iconSource: UM.Theme.getIcon("arrow_bottom"); - style: monitorButtonStyle - width: height - height: UM.Theme.getSize("setting_control").height - - onClicked: - { - connectedPrinter.moveHead(0, 0, -distancesRow.currentDistance) - } - } - } - } - - Row - { - id: distancesRow - - width: base.width - 2 * UM.Theme.getSize("default_margin").width - height: childrenRect.height + UM.Theme.getSize("default_margin").width - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - - spacing: UM.Theme.getSize("default_margin").width - - property real currentDistance: 10 - - Label - { - text: catalog.i18nc("@label", "Jog Distance") - color: UM.Theme.getColor("setting_control_text") - font: UM.Theme.getFont("default") - - width: Math.floor(parent.width * 0.4) - UM.Theme.getSize("default_margin").width - height: UM.Theme.getSize("setting_control").height - verticalAlignment: Text.AlignVCenter - } - - Row - { - Repeater - { - model: distancesModel - delegate: Button - { - height: UM.Theme.getSize("setting_control").height - width: height + UM.Theme.getSize("default_margin").width - - text: model.label - exclusiveGroup: distanceGroup - checkable: true - checked: distancesRow.currentDistance == model.value - onClicked: distancesRow.currentDistance = model.value - - style: ButtonStyle { - background: Rectangle { - border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if (control.checked || control.pressed) - { - return UM.Theme.getColor("action_button_active_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_border"); - } - return UM.Theme.getColor("action_button_border"); - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if (control.checked || control.pressed) - { - return UM.Theme.getColor("action_button_active"); - } - else if (control.hovered) - { - return UM.Theme.getColor("action_button_hovered"); - } - return UM.Theme.getColor("action_button"); - } - Behavior on color { ColorAnimation { duration: 50; } } - Label { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: UM.Theme.getSize("default_lining").width * 2 - anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2 - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else if (control.checked || control.pressed) - { - return UM.Theme.getColor("action_button_active_text"); - } - else if (control.hovered) - { - return UM.Theme.getColor("action_button_hovered_text"); - } - return UM.Theme.getColor("action_button_text"); - } - font: UM.Theme.getFont("default") - text: control.text - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideMiddle - } - } - label: Item { } - } - } - } - } - } - - ListModel - { - id: distancesModel - ListElement { label: "0.1"; value: 0.1 } - ListElement { label: "1"; value: 1 } - ListElement { label: "10"; value: 10 } - ListElement { label: "100"; value: 100 } - } - ExclusiveGroup { id: distanceGroup } + printerModel: activePrinter + visible: activePrinter != null ? activePrinter.canControlManually : false } - Loader + MonitorSection { - sourceComponent: monitorSection - property string label: catalog.i18nc("@label", "Active print") - } - Loader - { - sourceComponent: monitorItem - property string label: catalog.i18nc("@label", "Job Name") - property string value: activePrintJob != null ? activePrintJob.name : "" + label: catalog.i18nc("@label", "Active print") + width: base.width } - Loader + + MonitorItem { - sourceComponent: monitorItem - property string label: catalog.i18nc("@label", "Printing Time") - property string value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : "" + label: catalog.i18nc("@label", "Job Name") + value: activePrintJob != null ? activePrintJob.name : "" + width: base.width } - Loader + MonitorItem { - sourceComponent: monitorItem - property string label: catalog.i18nc("@label", "Estimated time left") - property string value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal - activePrintJob.timeElapsed) : "" + label: catalog.i18nc("@label", "Printing Time") + value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : "" + width:base.width + } + + MonitorItem + { + label: catalog.i18nc("@label", "Estimated time left") + value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal - activePrintJob.timeElapsed) : "" visible: { if(activePrintJob == null) @@ -466,141 +139,6 @@ Column activePrintJob.state == "pausing" || activePrintJob.state == "paused") } - } - - Component - { - id: monitorItem - - Row - { - height: UM.Theme.getSize("setting_control").height - width: Math.floor(base.width - 2 * UM.Theme.getSize("default_margin").width) - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - - Label - { - width: Math.floor(parent.width * 0.4) - anchors.verticalCenter: parent.verticalCenter - text: label - color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") - font: UM.Theme.getFont("default") - elide: Text.ElideRight - } - Label - { - width: Math.floor(parent.width * 0.6) - anchors.verticalCenter: parent.verticalCenter - text: value - color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") - font: UM.Theme.getFont("default") - elide: Text.ElideRight - } - } - } - - Component - { - id: monitorSection - - Rectangle - { - color: UM.Theme.getColor("setting_category") - width: base.width - height: UM.Theme.getSize("section").height - - Label - { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - text: label - font: UM.Theme.getFont("setting_category") - color: UM.Theme.getColor("setting_category_text") - } - } - } - - Component - { - id: monitorButtonStyle - - ButtonStyle - { - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_border"); - } - return UM.Theme.getColor("action_button_border"); - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered"); - } - return UM.Theme.getColor("action_button"); - } - Behavior on color - { - ColorAnimation - { - duration: 50 - } - } - } - - label: Item - { - UM.RecolorImage - { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: Math.floor(control.width / 2) - height: Math.floor(control.height / 2) - sourceSize.width: width - sourceSize.height: width - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_text"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_text"); - } - return UM.Theme.getColor("action_button_text"); - } - source: control.iconSource - } - } - } + width: base.width } } \ No newline at end of file diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index de34fe5943..5f09160708 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -234,7 +234,7 @@ Item return // Nothing to do, printer cant preheat at all! } preheatCountdown.text = "" - if (printerModel != null) + if (printerModel != null && connectedPrinter.preheatBedRemainingTime != null) { preheatCountdown.text = connectedPrinter.preheatBedRemainingTime; } diff --git a/resources/qml/PrinterOutput/ManualPrinterControl.qml b/resources/qml/PrinterOutput/ManualPrinterControl.qml new file mode 100644 index 0000000000..35cefe053f --- /dev/null +++ b/resources/qml/PrinterOutput/ManualPrinterControl.qml @@ -0,0 +1,442 @@ +// Copyright (c) 2017 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 + +import UM 1.2 as UM +import Cura 1.0 as Cura + + +Item +{ + property var printerModel + property var activePrintJob: printerModel != null ? printerModel.activePrintJob : null + implicitWidth: parent.width + implicitHeight: childrenRect.height + + Component + { + id: monitorButtonStyle + + ButtonStyle + { + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + border.color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_border"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_border"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_border"); + } + return UM.Theme.getColor("action_button_border"); + } + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered"); + } + return UM.Theme.getColor("action_button"); + } + Behavior on color + { + ColorAnimation + { + duration: 50 + } + } + } + + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2) + height: Math.floor(control.height / 2) + sourceSize.width: width + sourceSize.height: width + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_text"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_text"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_text"); + } + return UM.Theme.getColor("action_button_text"); + } + source: control.iconSource + } + } + } + } + + Column + { + enabled: + { + if (printerModel == null) + { + return false; //Can't control the printer if not connected + } + + if (!connectedPrinter.acceptsCommands) + { + return false; //Not allowed to do anything. + } + + if(activePrintJob == null) + { + return true + } + + if (activePrintJob.state == "printing" || activePrintJob.state == "resuming" || activePrintJob.state == "pausing" || activePrintJob.state == "error" || activePrintJob.state == "offline") + { + return false; //Printer is in a state where it can't react to manual control + } + return true; + } + + + MonitorSection + { + label: catalog.i18nc("@label", "Printer control") + width: base.width + } + + Row + { + width: base.width - 2 * UM.Theme.getSize("default_margin").width + height: childrenRect.height + UM.Theme.getSize("default_margin").width + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + + spacing: UM.Theme.getSize("default_margin").width + + Label + { + text: catalog.i18nc("@label", "Jog Position") + color: UM.Theme.getColor("setting_control_text") + font: UM.Theme.getFont("default") + + width: Math.floor(parent.width * 0.4) - UM.Theme.getSize("default_margin").width + height: UM.Theme.getSize("setting_control").height + verticalAlignment: Text.AlignVCenter + } + + GridLayout + { + columns: 3 + rows: 4 + rowSpacing: UM.Theme.getSize("default_lining").width + columnSpacing: UM.Theme.getSize("default_lining").height + + Label + { + text: catalog.i18nc("@label", "X/Y") + color: UM.Theme.getColor("setting_control_text") + font: UM.Theme.getFont("default") + width: height + height: UM.Theme.getSize("setting_control").height + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + Layout.row: 1 + Layout.column: 2 + Layout.preferredWidth: width + Layout.preferredHeight: height + } + + Button + { + Layout.row: 2 + Layout.column: 2 + Layout.preferredWidth: width + Layout.preferredHeight: height + iconSource: UM.Theme.getIcon("arrow_top"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.moveHead(0, distancesRow.currentDistance, 0) + } + } + + Button + { + Layout.row: 3 + Layout.column: 1 + Layout.preferredWidth: width + Layout.preferredHeight: height + iconSource: UM.Theme.getIcon("arrow_left"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.moveHead(-distancesRow.currentDistance, 0, 0) + } + } + + Button + { + Layout.row: 3 + Layout.column: 3 + Layout.preferredWidth: width + Layout.preferredHeight: height + iconSource: UM.Theme.getIcon("arrow_right"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.moveHead(distancesRow.currentDistance, 0, 0) + } + } + + Button + { + Layout.row: 4 + Layout.column: 2 + Layout.preferredWidth: width + Layout.preferredHeight: height + iconSource: UM.Theme.getIcon("arrow_bottom"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.moveHead(0, -distancesRow.currentDistance, 0) + } + } + + Button + { + Layout.row: 3 + Layout.column: 2 + Layout.preferredWidth: width + Layout.preferredHeight: height + iconSource: UM.Theme.getIcon("home"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.homeHead() + } + } + } + + + Column + { + spacing: UM.Theme.getSize("default_lining").height + + Label + { + text: catalog.i18nc("@label", "Z") + color: UM.Theme.getColor("setting_control_text") + font: UM.Theme.getFont("default") + width: UM.Theme.getSize("section").height + height: UM.Theme.getSize("setting_control").height + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Button + { + iconSource: UM.Theme.getIcon("arrow_top"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.moveHead(0, 0, distancesRow.currentDistance) + } + } + + Button + { + iconSource: UM.Theme.getIcon("home"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.homeBed() + } + } + + Button + { + iconSource: UM.Theme.getIcon("arrow_bottom"); + style: monitorButtonStyle + width: height + height: UM.Theme.getSize("setting_control").height + + onClicked: + { + connectedPrinter.moveHead(0, 0, -distancesRow.currentDistance) + } + } + } + } + + Row + { + id: distancesRow + + width: base.width - 2 * UM.Theme.getSize("default_margin").width + height: childrenRect.height + UM.Theme.getSize("default_margin").width + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + + spacing: UM.Theme.getSize("default_margin").width + + property real currentDistance: 10 + + Label + { + text: catalog.i18nc("@label", "Jog Distance") + color: UM.Theme.getColor("setting_control_text") + font: UM.Theme.getFont("default") + + width: Math.floor(parent.width * 0.4) - UM.Theme.getSize("default_margin").width + height: UM.Theme.getSize("setting_control").height + verticalAlignment: Text.AlignVCenter + } + + Row + { + Repeater + { + model: distancesModel + delegate: Button + { + height: UM.Theme.getSize("setting_control").height + width: height + UM.Theme.getSize("default_margin").width + + text: model.label + exclusiveGroup: distanceGroup + checkable: true + checked: distancesRow.currentDistance == model.value + onClicked: distancesRow.currentDistance = model.value + + style: ButtonStyle { + background: Rectangle { + border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width + border.color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_border"); + } + else if (control.checked || control.pressed) + { + return UM.Theme.getColor("action_button_active_border"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_border"); + } + return UM.Theme.getColor("action_button_border"); + } + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled"); + } + else if (control.checked || control.pressed) + { + return UM.Theme.getColor("action_button_active"); + } + else if (control.hovered) + { + return UM.Theme.getColor("action_button_hovered"); + } + return UM.Theme.getColor("action_button"); + } + Behavior on color { ColorAnimation { duration: 50; } } + Label { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: UM.Theme.getSize("default_lining").width * 2 + anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2 + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_text"); + } + else if (control.checked || control.pressed) + { + return UM.Theme.getColor("action_button_active_text"); + } + else if (control.hovered) + { + return UM.Theme.getColor("action_button_hovered_text"); + } + return UM.Theme.getColor("action_button_text"); + } + font: UM.Theme.getFont("default") + text: control.text + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideMiddle + } + } + label: Item { } + } + } + } + } + } + + ListModel + { + id: distancesModel + ListElement { label: "0.1"; value: 0.1 } + ListElement { label: "1"; value: 1 } + ListElement { label: "10"; value: 10 } + ListElement { label: "100"; value: 100 } + } + ExclusiveGroup { id: distanceGroup } + } +} \ No newline at end of file diff --git a/resources/qml/PrinterOutput/MonitorItem.qml b/resources/qml/PrinterOutput/MonitorItem.qml new file mode 100644 index 0000000000..cad8d2f7f3 --- /dev/null +++ b/resources/qml/PrinterOutput/MonitorItem.qml @@ -0,0 +1,44 @@ +// Copyright (c) 2017 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Item +{ + property string label: "" + property string value: "" + height: childrenRect.height; + + Row + { + height: UM.Theme.getSize("setting_control").height + width: Math.floor(base.width - 2 * UM.Theme.getSize("default_margin").width) + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + + Label + { + width: Math.floor(parent.width * 0.4) + anchors.verticalCenter: parent.verticalCenter + text: label + color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") + font: UM.Theme.getFont("default") + elide: Text.ElideRight + } + Label + { + width: Math.floor(parent.width * 0.6) + anchors.verticalCenter: parent.verticalCenter + text: value + color: connectedPrinter != null && connectedPrinter.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") + font: UM.Theme.getFont("default") + elide: Text.ElideRight + } + } +} \ No newline at end of file diff --git a/resources/qml/PrinterOutput/MonitorSection.qml b/resources/qml/PrinterOutput/MonitorSection.qml new file mode 100644 index 0000000000..6ed762362d --- /dev/null +++ b/resources/qml/PrinterOutput/MonitorSection.qml @@ -0,0 +1,33 @@ +// Copyright (c) 2017 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Item +{ + id: base + property string label + height: childrenRect.height; + Rectangle + { + color: UM.Theme.getColor("setting_category") + width: base.width + height: UM.Theme.getSize("section").height + + Label + { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + text: label + font: UM.Theme.getFont("setting_category") + color: UM.Theme.getColor("setting_category_text") + } + } +} \ No newline at end of file From 00eeb835ac35e0718f39aaa605adf718908e790e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 10:22:30 +0100 Subject: [PATCH 044/135] ManualPrinterControl uses correct functions again CL-541 --- cura/PrinterOutput/PrinterOutputController.py | 22 +++++++++---------- .../LegacyUM3PrinterOutputController.py | 4 ++++ .../PrinterOutput/ManualPrinterControl.qml | 18 +++++++-------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 982c41f293..86ca10e2d3 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -1,6 +1,8 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from UM.Logger import Logger + MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel @@ -16,31 +18,29 @@ class PrinterOutputController: self.can_control_manually = True self._output_device = output_device - def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOuputModel", temperature: int): - # TODO: implement - pass + Logger.log("w", "Set target hotend temperature not implemented in controller") def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): - pass + Logger.log("w", "Set target bed temperature not implemented in controller") def setJobState(self, job: "PrintJobOutputModel", state: str): - pass + Logger.log("w", "Set job state not implemented in controller") def cancelPreheatBed(self, printer: "PrinterOutputModel"): - pass + Logger.log("w", "Cancel preheat bed not implemented in controller") def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): - pass + Logger.log("w", "Preheat bed not implemented in controller") def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed): - pass + Logger.log("w", "Set head position not implemented in controller") def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): - pass + Logger.log("w", "Move head not implemented in controller") def homeBed(self, printer): - pass + Logger.log("w", "Home bed not implemented in controller") def homeHead(self, printer): - pass \ No newline at end of file + Logger.log("w", "Home head not implemented in controller") \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py index e303d237ce..ae8c989643 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py @@ -3,6 +3,10 @@ from cura.PrinterOutput.PrinterOutputController import PrinterOutputController +MYPY = False +if MYPY: + from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel + class LegacyUM3PrinterOutputController(PrinterOutputController): def __init__(self, output_device): diff --git a/resources/qml/PrinterOutput/ManualPrinterControl.qml b/resources/qml/PrinterOutput/ManualPrinterControl.qml index 35cefe053f..43fa769fb5 100644 --- a/resources/qml/PrinterOutput/ManualPrinterControl.qml +++ b/resources/qml/PrinterOutput/ManualPrinterControl.qml @@ -108,7 +108,7 @@ Item return false; //Can't control the printer if not connected } - if (!connectedPrinter.acceptsCommands) + if (!connectedDevice.acceptsCommands) { return false; //Not allowed to do anything. } @@ -188,7 +188,7 @@ Item onClicked: { - connectedPrinter.moveHead(0, distancesRow.currentDistance, 0) + printerModel.moveHead(0, distancesRow.currentDistance, 0) } } @@ -205,7 +205,7 @@ Item onClicked: { - connectedPrinter.moveHead(-distancesRow.currentDistance, 0, 0) + printerModel.moveHead(-distancesRow.currentDistance, 0, 0) } } @@ -222,7 +222,7 @@ Item onClicked: { - connectedPrinter.moveHead(distancesRow.currentDistance, 0, 0) + printerModel.moveHead(distancesRow.currentDistance, 0, 0) } } @@ -239,7 +239,7 @@ Item onClicked: { - connectedPrinter.moveHead(0, -distancesRow.currentDistance, 0) + printerModel.moveHead(0, -distancesRow.currentDistance, 0) } } @@ -256,7 +256,7 @@ Item onClicked: { - connectedPrinter.homeHead() + printerModel.homeHead() } } } @@ -286,7 +286,7 @@ Item onClicked: { - connectedPrinter.moveHead(0, 0, distancesRow.currentDistance) + printerModel.moveHead(0, 0, distancesRow.currentDistance) } } @@ -299,7 +299,7 @@ Item onClicked: { - connectedPrinter.homeBed() + printerModel.homeBed() } } @@ -312,7 +312,7 @@ Item onClicked: { - connectedPrinter.moveHead(0, 0, -distancesRow.currentDistance) + printerModel.moveHead(0, 0, -distancesRow.currentDistance) } } } From f570ba046bcc77a73220a909be84181701fbdd1a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 11:02:56 +0100 Subject: [PATCH 045/135] Added rudimentary jogging controls for UM3 This needs a bit more love; The machine is a bit stupid when it comes to moving outside of build area. CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 6 +++++- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 7 +++++-- .../LegacyUM3PrinterOutputController.py | 12 ++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index d4b9d9c99a..1571be453c 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -68,21 +68,25 @@ class PrinterOutputModel(QObject): @pyqtProperty("long", "long", "long") @pyqtProperty("long", "long", "long", "long") def setHeadPosition(self, x, y, z, speed = 3000): + self.updateHeadPosition(x, y, z) self._controller.setHeadPosition(self, x, y, z, speed) @pyqtProperty("long") @pyqtProperty("long", "long") def setHeadX(self, x, speed = 3000): + self.updateHeadPosition(x, self._head_position.y, self._head_position.z) self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed) @pyqtProperty("long") @pyqtProperty("long", "long") def setHeadY(self, y, speed = 3000): + self.updateHeadPosition(self._head_position.x, y, self._head_position.z) self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed) @pyqtProperty("long") @pyqtProperty("long", "long") - def setHeadY(self, z, speed = 3000): + def setHeadZ(self, z, speed = 3000): + self.updateHeadPosition(self._head_position.x, self._head_position.y, z) self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed) @pyqtSlot("long", "long", "long") diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index de0a8d6eff..f830e28764 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -411,7 +411,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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._key) + 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() @@ -530,7 +530,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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._key) + 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) @@ -580,6 +580,9 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"]) printer.updatePrinterState(result["status"]) + 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] diff --git a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py index ae8c989643..54c126e5cc 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py @@ -6,6 +6,7 @@ from cura.PrinterOutput.PrinterOutputController import PrinterOutputController MYPY = False if MYPY: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel + from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel class LegacyUM3PrinterOutputController(PrinterOutputController): @@ -15,3 +16,14 @@ class LegacyUM3PrinterOutputController(PrinterOutputController): def setJobState(self, job: "PrintJobOutputModel", state: str): data = "{\"target\": \"%s\"}" % state self._output_device.put("print_job/state", data, onFinished=None) + + 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) From f791b53ad85f95164f33c55c73726fed42974d6e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 13:54:34 +0100 Subject: [PATCH 046/135] Enabled monitor items for ClusterOutputDevice again CL-541 --- cura/PrinterOutputDevice.py | 28 ++----------------- .../ClusterUM3OutputDevice.py | 12 ++++++-- .../UM3OutputDevicePlugin.py | 6 ++-- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 5b747d19bf..cc41123f77 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -106,39 +106,15 @@ class PrinterOutputDevice(QObject, OutputDevice): def _createControlViewFromQML(self): if not self._control_view_qml_path: return - - path = QUrl.fromLocalFile(self._control_view_qml_path) - - # Because of garbage collection we need to keep this referenced by python. - self._control_component = QQmlComponent(Application.getInstance()._engine, path) - - # Check if the context was already requested before (Printer output device might have multiple items in the future) - if self._qml_context is None: - self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext()) - self._qml_context.setContextProperty("OutputDevice", self) - - self._control_item = self._control_component.create(self._qml_context) if self._control_item is None: - Logger.log("e", "QQmlComponent status %s", self._control_component.status()) - Logger.log("e", "QQmlComponent error string %s", self._control_component.errorString()) + self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self}) def _createMonitorViewFromQML(self): if not self._monitor_view_qml_path: return - path = QUrl.fromLocalFile(self._monitor_view_qml_path) - # Because of garbage collection we need to keep this referenced by python. - self._monitor_component = QQmlComponent(Application.getInstance()._engine, path) - - # Check if the context was already requested before (Printer output device might have multiple items in the future) - if self._qml_context is None: - self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext()) - self._qml_context.setContextProperty("OutputDevice", self) - - self._monitor_item = self._monitor_component.create(self._qml_context) if self._monitor_item is None: - Logger.log("e", "QQmlComponent status %s", self._monitor_component.status()) - Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString()) + self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) ## Attempt to establish connection def connect(self): diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 91acdc28af..73ac25f2f1 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -1,3 +1,6 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from UM.Logger import Logger from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice @@ -5,10 +8,12 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel -import json - from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +import json +import os + + class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address, properties, parent = None): super().__init__(device_id = device_id, address = address, properties=properties, parent = parent) @@ -18,6 +23,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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") + def _update(self): if not super()._update(): return diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index aecbc1717c..13ab774577 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -108,11 +108,11 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # or "Legacy" UM3 device. cluster_size = int(properties.get(b"cluster_size", -1)) # TODO: For debug purposes; force it to be legacy printer. - device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) - '''if cluster_size > 0: + #device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) + if cluster_size > 0: device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) else: - device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)''' + device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) self._discovered_devices[device.getId()] = device self.discoveredDevicesChanged.emit() From cdfdaec492e31c808d8ccf7628ce55448cfa0278 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 14:03:53 +0100 Subject: [PATCH 047/135] ClusterUM3 now uses local material data as first source CL-541 --- .../ClusterUM3OutputDevice.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 73ac25f2f1..c98d17911c 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -3,6 +3,8 @@ from UM.Logger import Logger +from UM.Settings.ContainerRegistry import ContainerRegistry + from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel @@ -105,7 +107,26 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): material_data = extruder_data["material"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: - material = MaterialOutputModel(guid = material_data["guid"], type = material_data["material"], brand=material_data["brand"], color=material_data["color"]) + 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"])) + # Unknown material. + 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) else: From 7d9af8e3451c044acc242a27d4841ae3c5f5bf12 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 14:13:30 +0100 Subject: [PATCH 048/135] Added Opencontrol panel functions CL-541 --- plugins/UM3NetworkPrinting/ClusterMonitorItem.qml | 5 +++-- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml index e78c7d1cc9..ec18b19119 100644 --- a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml +++ b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml @@ -16,6 +16,7 @@ Component 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 @@ -93,10 +94,10 @@ Component } } - PrinterVideoStream + /*PrinterVideoStream { visible: OutputDevice.selectedPrinterName != "" anchors.fill:parent - } + }*/ } } diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c98d17911c..db7bb68976 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -11,6 +11,8 @@ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtCore import pyqtSlot, QUrl import json import os @@ -28,6 +30,16 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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") + @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")) + def _update(self): if not super()._update(): return From 52a137a68cc7716bec3863707bea757be3cc0852 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 14:42:45 +0100 Subject: [PATCH 049/135] Ensured that sidebar has the right properties to show again CL-541 --- cura/PrinterOutput/PrintJobOutputModel.py | 2 +- cura/PrinterOutput/PrinterOutputModel.py | 12 +++++++ .../UM3NetworkPrinting/ClusterControlItem.qml | 9 +++--- .../ClusterUM3OutputDevice.py | 32 ++++++++++++++++++- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 00641ab89a..9c96c45ca8 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -22,7 +22,7 @@ class PrintJobOutputModel(QObject): self._state = "" self._time_total = 0 self._time_elapsed = 0 - self._name = "" # Human readable name + self._name = name # Human readable name self._key = key # Unique identifier self._assigned_printer = None diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 1571be453c..8a6585469b 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -21,6 +21,7 @@ class PrinterOutputModel(QObject): nameChanged = pyqtSignal() headPositionChanged = pyqtSignal() keyChanged = pyqtSignal() + typeChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): super().__init__(parent) @@ -35,6 +36,17 @@ class PrinterOutputModel(QObject): self._printer_state = "unknown" + self._type = "" + + @pyqtProperty(str, notify = typeChanged) + def type(self): + return self._type + + def updateType(self, type): + if self._type != type: + self._type = type + self.typeChanged.emit() + @pyqtProperty(str, notify=keyChanged) def key(self): return self._key diff --git a/plugins/UM3NetworkPrinting/ClusterControlItem.qml b/plugins/UM3NetworkPrinting/ClusterControlItem.qml index 8ba7156da8..b42515de51 100644 --- a/plugins/UM3NetworkPrinting/ClusterControlItem.qml +++ b/plugins/UM3NetworkPrinting/ClusterControlItem.qml @@ -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 } diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index db7bb68976..8b3f065576 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -12,13 +12,15 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices -from PyQt5.QtCore import pyqtSlot, QUrl +from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty import json import os class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): + printJobsChanged = pyqtSignal() + printersChanged = 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/" @@ -40,6 +42,31 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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=printersChanged) + 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 + def _update(self): if not super()._update(): return @@ -82,6 +109,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): old_job.assignedPrinter.updateActivePrintJob(None) self._print_jobs = print_jobs_seen + self.printJobsChanged.emit() def _onGetPrintersDataFinished(self, reply: QNetworkReply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) @@ -92,6 +120,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): Logger.log("w", "Received an invalid printers state message: Not valid JSON.") return + # TODO: Ensure that printers that have been removed are also removed locally. for printer_data in result: uuid = printer_data["uuid"] @@ -107,6 +136,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printer.updateName(printer_data["friendly_name"]) printer.updateKey(uuid) + printer.updateType(printer_data["machine_variant"]) for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] From 5d3779da261642f7cc4d83e0bf2597bf5af58344 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 15:54:44 +0100 Subject: [PATCH 050/135] Update cluster view components CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 10 ++-- cura/PrinterOutputDevice.py | 4 ++ .../UM3NetworkPrinting/ClusterMonitorItem.qml | 9 ++-- .../ClusterUM3OutputDevice.py | 20 +++++-- .../LegacyUM3OutputDevice.py | 4 +- .../PrintCoreConfiguration.qml | 4 +- .../UM3NetworkPrinting/PrinterInfoBlock.qml | 52 ++++++++----------- resources/qml/MonitorButton.qml | 4 +- resources/qml/Topbar.qml | 2 +- 9 files changed, 59 insertions(+), 50 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 8a6585469b..cb2dc15ea0 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -16,7 +16,7 @@ if MYPY: class PrinterOutputModel(QObject): bedTemperatureChanged = pyqtSignal() targetBedTemperatureChanged = pyqtSignal() - printerStateChanged = pyqtSignal() + stateChanged = pyqtSignal() activePrintJobChanged = pyqtSignal() nameChanged = pyqtSignal() headPositionChanged = pyqtSignal() @@ -161,17 +161,17 @@ class PrinterOutputModel(QObject): self._active_print_job = print_job self.activePrintJobChanged.emit() - def updatePrinterState(self, printer_state): + def updateState(self, printer_state): if self._printer_state != printer_state: self._printer_state = printer_state - self.printerStateChanged.emit() + self.stateChanged.emit() @pyqtProperty(QObject, notify = activePrintJobChanged) def activePrintJob(self): return self._active_print_job - @pyqtProperty(str, notify=printerStateChanged) - def printerState(self): + @pyqtProperty(str, notify=stateChanged) + def state(self): return self._printer_state @pyqtProperty(int, notify = bedTemperatureChanged) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index cc41123f77..3ce9782355 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -86,6 +86,10 @@ class PrinterOutputDevice(QObject, OutputDevice): return self._printers[0] return None + @pyqtProperty("QVariantList", notify = printersChanged) + def printers(self): + return self._printers + @pyqtProperty(QObject, constant=True) def monitorItem(self): # Note that we specifically only check if the monitor component is created. diff --git a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml index ec18b19119..5d819d9450 100644 --- a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml +++ b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml @@ -12,7 +12,6 @@ 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. @@ -34,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 @@ -47,7 +46,7 @@ Component width: Math.min(800 * screenScaleFactor, maximumWidth) height: children.height - visible: OutputDevice.connectedPrinters.length != 0 + visible: OutputDevice.printers.length != 0 Label { @@ -80,7 +79,7 @@ Component anchors.fill: parent spacing: -UM.Theme.getSize("default_lining").height - model: OutputDevice.connectedPrinters + model: OutputDevice.printers delegate: PrinterInfoBlock { diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 8b3f065576..c43855ce61 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -20,7 +20,11 @@ import os class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() - printersChanged = 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/" @@ -32,6 +36,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + @pyqtSlot() def openPrintJobControlPanel(self): Logger.log("d", "Opening print job control panel...") @@ -54,7 +61,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def activePrintJobs(self): return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None] - @pyqtProperty("QVariantList", notify=printersChanged) + @pyqtProperty("QVariantList", notify=clusterPrintersChanged) def connectedPrintersTypeCount(self): printer_count = {} for printer in self._printers: @@ -119,7 +126,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid printers state message: Not valid JSON.") return - + printer_list_changed = False # TODO: Ensure that printers that have been removed are also removed locally. for printer_data in result: uuid = printer_data["uuid"] @@ -133,10 +140,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if printer is None: printer = PrinterOutputModel(output_controller=None, number_of_extruders=self._number_of_extruders) self._printers.append(printer) + printer_list_changed = True printer.updateName(printer_data["friendly_name"]) printer.updateKey(uuid) printer.updateType(printer_data["machine_variant"]) + if not printer_data["enabled"]: + printer.updateState("disabled") + else: + printer.updateState(printer_data["status"]) for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] @@ -171,6 +183,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): name = name) extruder.updateActiveMaterial(material) + if printer_list_changed: + self.printersChanged.emit() else: Logger.log("w", "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index f830e28764..e1acd1bede 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -159,7 +159,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # No active printer. Unable to write return - if self.activePrinter.printerState not in ["idle", ""]: + if self.activePrinter.state not in ["idle", ""]: # Printer is not able to accept commands. return @@ -578,7 +578,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): printer = self._printers[0] printer.updateBedTemperature(result["bed"]["temperature"]["current"]) printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"]) - printer.updatePrinterState(result["status"]) + printer.updateState(result["status"]) head_position = result["heads"][0]["position"] printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"]) diff --git a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml index 03ff4542e1..abebca2eb8 100644 --- a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml +++ b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml @@ -15,7 +15,7 @@ Item Label { id: materialLabel - text: printCoreConfiguration.material.material + " (" + printCoreConfiguration.material.color + ")" + text: printCoreConfiguration.activeMaterial.type + " (" + printCoreConfiguration.activeMaterial.color + ")" 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 diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml index c253ebae89..a879ff7491 100644 --- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -31,7 +31,7 @@ Rectangle function printerStatusText(printer) { - switch (printer.status) + switch (printer.state) { case "pre_print": return catalog.i18nc("@label", "Preparing to print") @@ -49,22 +49,14 @@ 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 @@ -73,7 +65,7 @@ Rectangle hoverEnabled: true; // Only clickable if no printer is selected - enabled: OutputDevice.selectedPrinterName == "" && printer.status !== "unreachable" + enabled: OutputDevice.selectedPrinterName == "" && printer.state !== "unreachable" } Row @@ -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": @@ -328,26 +320,26 @@ 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"; } } - else if (printJob.status === "wait_cleanup") + else if (printJob.state === "wait_cleanup") { return "checkmark-icon.svg"; } @@ -384,19 +376,19 @@ 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": @@ -432,7 +424,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) } diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index 6166f9b62f..a60eb0b3f3 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -73,7 +73,7 @@ Item if(!printerConnected || !printerAcceptsCommands) return UM.Theme.getColor("text"); - switch(activePrinter.printerState) + switch(activePrinter.state) { case "maintenance": return UM.Theme.getColor("status_busy"); @@ -118,7 +118,7 @@ Item var printerOutputDevice = Cura.MachineManager.printerOutputDevices[0] - if(activePrinter.printerState == "maintenance") + if(activePrinter.state == "maintenance") { return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer"); } diff --git a/resources/qml/Topbar.qml b/resources/qml/Topbar.qml index 63d0981830..4b5008c43e 100644 --- a/resources/qml/Topbar.qml +++ b/resources/qml/Topbar.qml @@ -124,7 +124,7 @@ Rectangle { return UM.Theme.getIcon("tab_status_unknown"); } - if (Cura.MachineManager.printerOutputDevices[0].printerState == "maintenance") + if (Cura.MachineManager.printerOutputDevices[0].state == "maintenance") { return UM.Theme.getIcon("tab_status_busy"); } From 6f495f2d8b1cadbc0848e8e241e59cb625fa5641 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 16:00:57 +0100 Subject: [PATCH 051/135] Cluster monitor now uses material name This matches better with what Cura does. CL-541 --- plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml index abebca2eb8..f0aeebd217 100644 --- a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml +++ b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml @@ -15,7 +15,7 @@ Item Label { id: materialLabel - text: printCoreConfiguration.activeMaterial.type + " (" + printCoreConfiguration.activeMaterial.color + ")" + text: printCoreConfiguration.activeMaterial.name elide: Text.ElideRight width: parent.width font: UM.Theme.getFont("very_small") From c6f2e167e20c96fece8e83b1adedf0aea3cfd748 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 16:02:55 +0100 Subject: [PATCH 052/135] Renamed some missed properties CL-541 --- plugins/UM3NetworkPrinting/PrinterInfoBlock.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml index a879ff7491..b6b4f2e8c4 100644 --- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -132,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 } @@ -142,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") From 83b13546fbb72584cc075eb42f1e4060a93e6bdf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 17:12:36 +0100 Subject: [PATCH 053/135] Moved compressing of GCode to one class higher CL-541 --- .../NetworkedPrinterOutputDevice.py | 32 ++++++++++++ cura/PrinterOutputDevice.py | 2 +- .../ClusterUM3OutputDevice.py | 51 ++++++++++++++++++- .../LegacyUM3OutputDevice.py | 32 ------------ 4 files changed, 82 insertions(+), 35 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 58c82b6c38..e38338172a 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -45,6 +45,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._cached_multiparts = {} + self._sending_gcode = False + self._compressing_gcode = False + self._gcode = [] + def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): raise NotImplementedError("requestWrite needs to be implemented") @@ -57,6 +61,34 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def authenticationState(self): return self._authentication_state + def _compressGCode(self): + self._compressing_gcode = True + + ## Mash the data into single string + max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line. + byte_array_file_data = b"" + batched_line = "" + + for line in self._gcode: + if not self._compressing_gcode: + self._progress_message.hide() + # Stop trying to zip / send as abort was called. + return + 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 += self.__compressDataAndNotifyQt(batched_line) + batched_line = "" + + # Don't miss the last batch (If any) + if batched_line: + byte_array_file_data += self.__compressDataAndNotifyQt(batched_line) + + self._compressing_gcode = False + return byte_array_file_data + def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 3ce9782355..bf912ad4a5 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -138,7 +138,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def acceptsCommands(self): return self._accepts_commands - ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands + ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands def setAcceptsCommands(self, accepts_commands): if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c43855ce61..7d95acc920 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -2,10 +2,12 @@ # 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 cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice +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 @@ -17,6 +19,8 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty import json import os +i18n_catalog = i18nCatalog("cura") + class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() @@ -39,6 +43,49 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # 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 + + 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().showPrintMonitor.emit(True) + 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 + + @pyqtSlot() + def sendPrintJob(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._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) + + compressed_gcode = self._compressGCode() + if compressed_gcode is None: + # Abort was called. + return + + + + @pyqtSlot() def openPrintJobControlPanel(self): Logger.log("d", "Opening print job control panel...") diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index e1acd1bede..642a67d729 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -63,10 +63,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_succeeded_message = None self._not_authenticated_message = None - self._sending_gcode = False - self._compressing_gcode = False - self._gcode = [] - self.authenticationStateChanged.connect(self._onAuthenticationStateChanged) self.setPriority(3) # Make sure the output device gets selected above local file output @@ -286,34 +282,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._progress_message.hide() - def _compressGCode(self): - self._compressing_gcode = True - - ## Mash the data into single string - max_chars_per_line = 1024 * 1024 / 4 # 1/4 MB per line. - byte_array_file_data = b"" - batched_line = "" - - for line in self._gcode: - if not self._compressing_gcode: - self._progress_message.hide() - # Stop trying to zip / send as abort was called. - return - 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 += self.__compressDataAndNotifyQt(batched_line) - batched_line = "" - - # Don't miss the last batch (If any) - if batched_line: - byte_array_file_data += self.__compressDataAndNotifyQt(batched_line) - - self._compressing_gcode = False - return byte_array_file_data - def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: From c1c59925ded42732d6d0e63a9ce9811c6d494dfc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 27 Nov 2017 17:14:30 +0100 Subject: [PATCH 054/135] Removed duplicated code CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index e38338172a..91da01d9cb 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -113,16 +113,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): return True - def _createEmptyFormRequest(self, target): + def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json"): url = QUrl("http://" + self._address + self._api_prefix + target) request = QNetworkRequest(url) - request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) - return request - - def _createEmptyRequest(self, target): - url = QUrl("http://" + self._address + self._api_prefix + target) - request = QNetworkRequest(url) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + if content_type is not None: + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request @@ -168,7 +163,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() - request = self._createEmptyFormRequest(target) + request = self._createEmptyRequest(target, content_type=None) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) post_part = QHttpPart() From cfc6a3ad484658ffeae1be5849aa567a7519a763 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Nov 2017 12:43:49 +0100 Subject: [PATCH 055/135] Added some convenience functions to NetworkedPrinterOutputdevice This also moves the getUser from legacy to networked printer CL-541 --- .../NetworkedPrinterOutputDevice.py | 43 +++++++++++++++---- .../LegacyUM3OutputDevice.py | 12 +----- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 91da01d9cb..8d6e39bf35 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -12,7 +12,9 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, py from time import time from typing import Callable, Any, Optional from enum import IntEnum +from typing import List +import os class AuthState(IntEnum): NotAuthenticated = 1 @@ -121,6 +123,28 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request + def _createFormPart(self, content_header, data, content_type = None): + part = QHttpPart() + + if not content_header.startswith("form-data;"): + content_header = "form_data; " + content_header + part.setHeader(QNetworkRequest.ContentDispositionHeader, content_header) + + if content_type is not None: + part.setHeader(QNetworkRequest.ContentTypeHeader, content_type) + + part.setBody(data) + return part + + ## Convenience function to get the username from the OS. + # The code was copied from the getpass module, as we try to use as little dependencies as possible. + def _getUserName(self): + for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): + user = os.environ.get(name) + if user: + return user + return "Unknown User" # Couldn't find out username. + def _clearCachedMultiPart(self, reply): if id(reply) in self._cached_multiparts: del self._cached_multiparts[id(reply)] @@ -160,29 +184,32 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target, content_type=None) - multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) - post_part = QHttpPart() - post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) - post_part.setBody(body_data) - multi_post_part.append(post_part) + for part in parts: + multi_post_part.append(part) self._last_request_time = time() reply = self._manager.post(request, multi_post_part) - # Due to garbage collection on python doing some weird stuff, we need to keep hold of a reference - self._cached_multiparts[id(reply)] = (post_part, multi_post_part, reply) + self._cached_multiparts[id(reply)] = (multi_post_part, reply) if onProgress is not None: reply.uploadProgress.connect(onProgress) if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + post_part = QHttpPart() + post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) + post_part.setBody(body_data) + + self.postFormWithParts(target, [post_part], onFinished, onProgress) + def _onAuthenticationRequired(self, reply, authenticator): Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString())) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 642a67d729..35e7f1890d 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -143,6 +143,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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: @@ -596,13 +597,4 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): result = "********" + result return result - return self._authentication_key - - ## Convenience function to get the username from the OS. - # The code was copied from the getpass module, as we try to use as little dependencies as possible. - def _getUserName(self): - for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): - user = os.environ.get(name) - if user: - return user - return "Unknown User" # Couldn't find out username. \ No newline at end of file + return self._authentication_key \ No newline at end of file From 9084dfd6bd3545e57618b1300d7e19b593f4db6b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Nov 2017 12:59:54 +0100 Subject: [PATCH 056/135] It's now possible to send print jobs to cluster again CL-541 --- .../NetworkedPrinterOutputDevice.py | 20 ++++++--- .../ClusterUM3OutputDevice.py | 43 +++++++++++++++++++ .../LegacyUM3OutputDevice.py | 13 +----- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 8d6e39bf35..3585aee5ea 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -7,14 +7,14 @@ from UM.Logger import Logger from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl - +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtSignal, QUrl, QCoreApplication from time import time from typing import Callable, Any, Optional from enum import IntEnum from typing import List -import os +import os # To get the username +import gzip class AuthState(IntEnum): NotAuthenticated = 1 @@ -63,6 +63,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def authenticationState(self): return self._authentication_state + def _compressDataAndNotifyQt(self, 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. + # If we don't do this, the device might trigger a timeout. + self._last_response_time = time() + return compressed_data + def _compressGCode(self): self._compressing_gcode = True @@ -81,12 +91,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): # 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 += self.__compressDataAndNotifyQt(batched_line) + byte_array_file_data += self._compressDataAndNotifyQt(batched_line) batched_line = "" # Don't miss the last batch (If any) if batched_line: - byte_array_file_data += self.__compressDataAndNotifyQt(batched_line) + byte_array_file_data += self._compressDataAndNotifyQt(batched_line) self._compressing_gcode = False return byte_array_file_data diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 7d95acc920..ec6c94adb7 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -16,6 +16,8 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty +from time import time + import json import os @@ -61,6 +63,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Unable to find g-code. Nothing to send return + # TODO; DEBUG + self.sendPrintJob() + @pyqtSlot() def sendPrintJob(self): Logger.log("i", "Sending print job to printer.") @@ -83,8 +88,46 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Abort was called. return + parts = [] + # If a specific printer was selected, it should be printed with that machine. + require_printer_name = "" # Todo; actually needs to be set + if require_printer_name: + parts.append(self._createFormPart("name=require_printer_name", bytes(require_printer_name, "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) + + def _onPostPrintJobFinished(self, reply): + print("POST PRINTJOB DONE! YAY!", reply.readAll()) + pass + + 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().showPrintMonitor.emit(False) @pyqtSlot() def openPrintJobControlPanel(self): diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 35e7f1890d..d89efc2acc 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -21,8 +21,7 @@ from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController from time import time import json -import os # To get the username -import gzip + i18n_catalog = i18nCatalog("cura") @@ -259,16 +258,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._progress_message.hide() self._sending_gcode = False - def __compressDataAndNotifyQt(self, 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. - # If we don't do this, the device might trigger a timeout. - self._last_response_time = time() - return compressed_data - def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 From e8418960903aa68a06a19e17490a358c48debda6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 28 Nov 2017 13:04:58 +0100 Subject: [PATCH 057/135] PrintJobs are now assigned if they are not queued It used to just do it if it was printing, but jobs can also be in other states such as paused, pre_print, etc CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index ec6c94adb7..c8462e34d5 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -193,7 +193,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_job.updateTimeTotal(print_job_data["time_total"]) print_job.updateTimeElapsed(print_job_data["time_elapsed"]) print_job.updateState(print_job_data["status"]) - if print_job.state == "printing": + if print_job.state != "queued": # Print job should be assigned to a printer. printer = self._getPrinterByKey(print_job_data["printer_uuid"]) if printer: From 3d3b140526191126b3a7e2cf45fd1180861c6bcb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 10:53:38 +0100 Subject: [PATCH 058/135] Times are correctly displayed for Cluster again CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 7 ++++++- plugins/UM3NetworkPrinting/PrinterInfoBlock.qml | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c8462e34d5..ce96627296 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -6,6 +6,7 @@ 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 @@ -164,6 +165,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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) + def _update(self): if not super()._update(): return @@ -201,7 +206,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_jobs_seen.append(print_job) for old_job in self._print_jobs: - if old_job not in print_jobs_seen: + if old_job not in print_jobs_seen and old_job.assignedPrinter: # Print job needs to be removed. old_job.assignedPrinter.updateActivePrintJob(None) diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml index b6b4f2e8c4..5c0963a390 100644 --- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -22,11 +22,11 @@ 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) @@ -114,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 From f30f0a7194366d02492d472069f6783bef3a65d0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 11:04:13 +0100 Subject: [PATCH 059/135] Mismatched configuration is now shown correctly again CL-541 --- cura/PrinterOutput/PrintJobOutputModel.py | 14 +++++++++++++- .../ClusterUM3OutputDevice.py | 10 ++++++++-- plugins/UM3NetworkPrinting/PrinterInfoBlock.qml | 17 ++--------------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 9c96c45ca8..fa8bbe8673 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot +from typing import Optional MYPY = False if MYPY: from cura.PrinterOutput.PrinterOutputController import PrinterOutputController @@ -15,6 +16,7 @@ class PrintJobOutputModel(QObject): nameChanged = pyqtSignal() keyChanged = pyqtSignal() assignedPrinterChanged = pyqtSignal() + ownerChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None): super().__init__(parent) @@ -24,7 +26,17 @@ class PrintJobOutputModel(QObject): self._time_elapsed = 0 self._name = name # Human readable name self._key = key # Unique identifier - self._assigned_printer = None + self._assigned_printer = None # type: Optional[PrinterOutputModel] + self._owner = "" # Who started/owns the print job? + + @pyqtProperty(str, notify=ownerChanged) + def owner(self): + return self._owner + + def updateOwner(self, owner): + if self._owner != owner: + self._owner = owner + self.ownerChanged.emit() @pyqtProperty(QObject, notify=assignedPrinterChanged) def assignedPrinter(self): diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index ce96627296..362dc344d4 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -198,11 +198,17 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_job.updateTimeTotal(print_job_data["time_total"]) print_job.updateTimeElapsed(print_job_data["time_elapsed"]) print_job.updateState(print_job_data["status"]) + print_job.updateOwner(print_job_data["owner"]) + printer = None if print_job.state != "queued": # Print job should be assigned to a printer. printer = self._getPrinterByKey(print_job_data["printer_uuid"]) - if printer: - printer.updateActivePrintJob(print_job) + else: # Status is queued + # 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) for old_job in self._print_jobs: diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml index 5c0963a390..29fe8882f9 100644 --- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -269,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"); @@ -334,10 +327,7 @@ Rectangle { 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.state === "wait_cleanup") { @@ -401,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 ""; } From 70cfbf01809abb1d25132d19364ca1f2dda39a25 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 11:44:18 +0100 Subject: [PATCH 060/135] PostPrintjobFinished now hides messages & resets state CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 6 ++++-- plugins/UM3NetworkPrinting/PrinterInfoBlock.qml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 362dc344d4..7d459abf4c 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -106,8 +106,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress) def _onPostPrintJobFinished(self, reply): - print("POST PRINTJOB DONE! YAY!", reply.readAll()) - pass + self._progress_message.hide() + self._compressing_gcode = False + self._sending_gcode = False + Application.getInstance().showPrintMonitor.emit(False) def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml index 29fe8882f9..3a7fd1fc74 100644 --- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -382,7 +382,7 @@ Rectangle { 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": From a49d3dbd8ec654834d86d4e7bdba6aca0c7775b3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 11:57:14 +0100 Subject: [PATCH 061/135] Added missing time/date completed CL-541 --- .../UM3NetworkPrinting/ClusterUM3OutputDevice.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 7d459abf4c..76d4c70752 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -18,7 +18,7 @@ from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty from time import time - +from datetime import datetime import json import os @@ -171,6 +171,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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 _update(self): if not super()._update(): return From dea13899b321825fe22e0809cb7b8bdd260f1b09 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 13:07:01 +0100 Subject: [PATCH 062/135] Cluster can now "target" one of it's printers for display CL-541 --- .../UM3NetworkPrinting/ClusterMonitorItem.qml | 7 +++---- .../ClusterUM3OutputDevice.py | 18 ++++++++++++++++-- .../UM3NetworkPrinting/PrinterInfoBlock.qml | 4 ++-- .../UM3NetworkPrinting/PrinterVideoStream.qml | 2 +- resources/qml/MonitorButton.qml | 13 +++++++++---- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml index 5d819d9450..df102915ff 100644 --- a/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml +++ b/plugins/UM3NetworkPrinting/ClusterMonitorItem.qml @@ -62,7 +62,6 @@ Component } } - ScrollView { id: printerScrollView @@ -93,10 +92,10 @@ Component } } - /*PrinterVideoStream + PrinterVideoStream { - visible: OutputDevice.selectedPrinterName != "" + visible: OutputDevice.activePrinter != null anchors.fill:parent - }*/ + } } } diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 76d4c70752..3cee20a54f 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -15,10 +15,12 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices -from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty +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 @@ -27,6 +29,7 @@ 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. @@ -54,6 +57,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._error_message = None self._progress_message = None + self._active_printer = None # type: Optional[PrinterOutputModel] + 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().showPrintMonitor.emit(True) @@ -105,11 +110,20 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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: + self._active_printer = printer + self.activePrinterChanged.emit() + def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - Application.getInstance().showPrintMonitor.emit(False) def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: diff --git a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml index 3a7fd1fc74..6d7d6c8a7d 100644 --- a/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml +++ b/plugins/UM3NetworkPrinting/PrinterInfoBlock.qml @@ -61,11 +61,11 @@ Rectangle { 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.state !== "unreachable" + enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable" } Row diff --git a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml index 6793d74ac5..d0a9e08232 100644 --- a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml +++ b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml @@ -17,7 +17,7 @@ Item MouseArea { anchors.fill: parent - onClicked: OutputDevice.selectAutomaticPrinter() + onClicked: OutputDevice.setActivePrinter(null) z: 0 } diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index a60eb0b3f3..778884ba00 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -70,8 +70,10 @@ Item property variant statusColor: { - if(!printerConnected || !printerAcceptsCommands) + if(!printerConnected || !printerAcceptsCommands || activePrinter == null) + { return UM.Theme.getColor("text"); + } switch(activePrinter.state) { @@ -117,7 +119,10 @@ Item } var printerOutputDevice = Cura.MachineManager.printerOutputDevices[0] - + if(activePrinter == null) + { + return ""; + } if(activePrinter.state == "maintenance") { return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer"); @@ -262,7 +267,7 @@ Item property bool userClicked: false property string lastJobState: "" - visible: printerConnected && activePrinter.canPause + visible: printerConnected && activePrinter != null &&activePrinter.canPause enabled: (!userClicked) && printerConnected && printerAcceptsCommands && activePrintJob != null && (["paused", "printing"].indexOf(activePrintJob.state) >= 0) @@ -305,7 +310,7 @@ Item { id: abortButton - visible: printerConnected && activePrinter.canAbort + visible: printerConnected && activePrinter != null && activePrinter.canAbort enabled: printerConnected && printerAcceptsCommands && activePrintJob != null && (["paused", "printing", "pre_print"].indexOf(activePrintJob.state) >= 0) From d8c48343625ac745b18576b9761273b8af5cb7c3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 13:20:58 +0100 Subject: [PATCH 063/135] Aborting & pausing prints is now possible again from the Cluster output device CL-541 --- .../ClusterUM3OutputDevice.py | 6 ++++-- .../ClusterUM3PrinterOutputController.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 3cee20a54f..da26e77643 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -13,6 +13,8 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel +from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController + from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject @@ -221,7 +223,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): break if print_job is None: - print_job = PrintJobOutputModel(output_controller = None, + print_job = PrintJobOutputModel(output_controller = ClusterUM3PrinterOutputController(self), key = print_job_data["uuid"], name = print_job_data["name"]) print_job.updateTimeTotal(print_job_data["time_total"]) @@ -268,7 +270,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): break if printer is None: - printer = PrinterOutputModel(output_controller=None, number_of_extruders=self._number_of_extruders) + printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) self._printers.append(printer) printer_list_changed = True diff --git a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py new file mode 100644 index 0000000000..4615cd62dc --- /dev/null +++ b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py @@ -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) + From 339d7ca4c9e5da8e7277f2a40237fae1b811ddec Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 14:36:53 +0100 Subject: [PATCH 064/135] Cluster shows default controlItem again when a specific printer is selected CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 8 ++++++++ resources/qml/PrintMonitor.qml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index da26e77643..f4675a2e0a 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -61,6 +61,14 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._active_printer = None # type: Optional[PrinterOutputModel] + @pyqtProperty(QObject, notify=activePrinterChanged) + def controlItem(self): + if self._active_printer is None: + return super().controlItem + else: + # Let cura use the default. + return None + 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().showPrintMonitor.emit(True) diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 830093cd2b..b3c36f7fd4 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -45,7 +45,7 @@ Column Repeater { id: extrudersRepeater - model: activePrinter.extruders + model: activePrinter!=null ? activePrinter.extruders : null ExtruderBox { From 1c1c195b931705e8746b28ab65f0411501fca4ce Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 15:01:28 +0100 Subject: [PATCH 065/135] When an printer gets added / removed, this is now correctly shown CL-541 --- .../ClusterUM3OutputDevice.py | 19 ++++++++++++++++++- .../PrintCoreConfiguration.qml | 2 +- resources/qml/MonitorButton.qml | 5 ++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index f4675a2e0a..b64716c958 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -268,6 +268,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return printer_list_changed = False # TODO: Ensure that printers that have been removed are also removed locally. + + printers_seen = [] + for printer_data in result: uuid = printer_data["uuid"] @@ -282,6 +285,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._printers.append(printer) printer_list_changed = True + printers_seen.append(printer) + printer.updateName(printer_data["friendly_name"]) printer.updateKey(uuid) printer.updateType(printer_data["machine_variant"]) @@ -292,7 +297,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] - extruder_data = printer_data["configuration"][index] + try: + extruder_data = printer_data["configuration"][index] + except IndexError: + break + try: hotend_id = extruder_data["print_core_id"] except KeyError: @@ -322,6 +331,14 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): color = color, name = name) extruder.updateActiveMaterial(material) + removed_printers = [printer for printer in self._printers if printer not in printers_seen] + + for removed_printer in removed_printers: + self._printers.remove(removed_printer) + printer_list_changed = True + if self._active_printer == removed_printer: + self._active_printer = None + self.activePrinterChanged.emit() if printer_list_changed: self.printersChanged.emit() diff --git a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml index f0aeebd217..70fa65da5e 100644 --- a/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml +++ b/plugins/UM3NetworkPrinting/PrintCoreConfiguration.qml @@ -15,7 +15,7 @@ Item Label { id: materialLabel - text: printCoreConfiguration.activeMaterial.name + text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : "" elide: Text.ElideRight width: parent.width font: UM.Theme.getFont("very_small") diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index 778884ba00..d4861c830a 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -82,7 +82,10 @@ Item case "error": return UM.Theme.getColor("status_stopped"); } - + if(base.activePrintJob == null) + { + return UM.Theme.getColor("text"); + } switch(base.activePrintJob.state) { case "printing": From 51c4062f1b4649833036f9d5cd2bbcbc0ca35947 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 30 Nov 2017 15:17:08 +0100 Subject: [PATCH 066/135] Limit the amount of emits happening for PrintJobs changed CL-541 --- .../ClusterUM3OutputDevice.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index b64716c958..8114a3eef5 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -223,6 +223,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): Logger.log("w", "Received an invalid print jobs message: Not valid JSON.") return print_jobs_seen = [] + job_list_changed = False for print_job_data in result: print_job = None for job in self._print_jobs: @@ -234,6 +235,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_job = PrintJobOutputModel(output_controller = ClusterUM3PrinterOutputController(self), key = print_job_data["uuid"], name = print_job_data["name"]) + job_list_changed = True + self._print_jobs.append(print_job) print_job.updateTimeTotal(print_job_data["time_total"]) print_job.updateTimeElapsed(print_job_data["time_elapsed"]) print_job.updateState(print_job_data["status"]) @@ -250,13 +253,18 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printer.updateActivePrintJob(print_job) print_jobs_seen.append(print_job) - for old_job in self._print_jobs: - if old_job not in print_jobs_seen and old_job.assignedPrinter: - # Print job needs to be removed. - old_job.assignedPrinter.updateActivePrintJob(None) - self._print_jobs = print_jobs_seen - self.printJobsChanged.emit() + # 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: + if removed_job.assignedPrinter: + removed_job.assignedPrinter.updateActivePrintJob(None) + self._print_jobs.remove(removed_job) + job_list_changed = True + + # Do a single emit for all print job changes. + if job_list_changed: + self.printJobsChanged.emit() def _onGetPrintersDataFinished(self, reply: QNetworkReply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) From 77e3965fc7ca3771bc421dac564849c95c48df52 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 6 Dec 2017 14:59:20 +0100 Subject: [PATCH 067/135] Added videostream to cluster again CL-541 --- cura/CameraImageProvider.py | 2 +- cura/PrinterOutput/NetworkCamera.py | 113 ++++++++++++++++++ cura/PrinterOutput/PrinterOutputModel.py | 12 ++ .../ClusterUM3OutputDevice.py | 2 + .../UM3NetworkPrinting/PrinterVideoStream.qml | 16 ++- 5 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 cura/PrinterOutput/NetworkCamera.py diff --git a/cura/CameraImageProvider.py b/cura/CameraImageProvider.py index ff66170f3c..ddf978f625 100644 --- a/cura/CameraImageProvider.py +++ b/cura/CameraImageProvider.py @@ -12,7 +12,7 @@ class CameraImageProvider(QQuickImageProvider): def requestImage(self, id, size): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): try: - return output_device.getCameraImage(), QSize(15, 15) + return output_device.activePrinter.camera.getImage(), QSize(15, 15) except AttributeError: pass return QImage(), QSize(15, 15) \ No newline at end of file diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py new file mode 100644 index 0000000000..5cb76d2876 --- /dev/null +++ b/cura/PrinterOutput/NetworkCamera.py @@ -0,0 +1,113 @@ +from UM.Logger import Logger + +from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot +from PyQt5.QtGui import QImage +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager + + +class NetworkCamera(QObject): + newImage = pyqtSignal() + + def __init__(self, target = None, parent = None): + super().__init__(parent) + self._stream_buffer = b"" + self._stream_buffer_start_index = -1 + self._manager = None + self._image_request = None + self._image_reply = None + self._image = QImage() + self._image_id = 0 + + self._target = target + self._started = False + + @pyqtSlot(str) + def setTarget(self, target): + restart_required = False + if self._started: + self.stop() + restart_required = True + + self._target = target + + if restart_required: + self.start() + + @pyqtProperty(QUrl, notify=newImage) + def latestImage(self): + self._image_id += 1 + # There is an image provider that is called "camera". In order to ensure that the image qml object, that + # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl + # as new (instead of relying on cached version and thus forces an update. + temp = "image://camera/" + str(self._image_id) + + return QUrl(temp, QUrl.TolerantMode) + + @pyqtSlot() + def start(self): + if self._target is None: + Logger.log("w", "Unable to start camera stream without target!") + return + self._started = True + url = QUrl(self._target) + self._image_request = QNetworkRequest(url) + if self._manager is None: + self._manager = QNetworkAccessManager() + + self._image_reply = self._manager.get(self._image_request) + self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) + + @pyqtSlot() + def stop(self): + self._manager = None + + self._stream_buffer = b"" + self._stream_buffer_start_index = -1 + + if self._image_reply: + try: + # disconnect the signal + try: + self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) + except Exception: + pass + # abort the request if it's not finished + if not self._image_reply.isFinished(): + self._image_reply.close() + except Exception as e: # RuntimeError + pass # It can happen that the wrapped c++ object is already deleted. + + self._image_reply = None + self._image_request = None + + self._started = False + + def getImage(self): + return self._image + + def _onStreamDownloadProgress(self, bytes_received, bytes_total): + # An MJPG stream is (for our purpose) a stream of concatenated JPG images. + # JPG images start with the marker 0xFFD8, and end with 0xFFD9 + if self._image_reply is None: + return + self._stream_buffer += self._image_reply.readAll() + + if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger + Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...") + self.stop() # resets stream buffer and start index + self.start() + return + + if self._stream_buffer_start_index == -1: + self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8') + stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9') + # If this happens to be more than a single frame, then so be it; the JPG decoder will + # ignore the extra data. We do it like this in order not to get a buildup of frames + + if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1: + jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2] + self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:] + self._stream_buffer_start_index = -1 + self._image.loadFromData(jpg_data) + + self.newImage.emit() diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index cb2dc15ea0..aaf9b48968 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -22,6 +22,7 @@ class PrinterOutputModel(QObject): headPositionChanged = pyqtSignal() keyChanged = pyqtSignal() typeChanged = pyqtSignal() + cameraChanged = pyqtSignal() def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): super().__init__(parent) @@ -38,6 +39,17 @@ class PrinterOutputModel(QObject): self._type = "" + self._camera = None + + def setCamera(self, camera): + if self._camera is not camera: + self._camera = camera + self.cameraChanged.emit() + + @pyqtProperty(QObject, notify=cameraChanged) + def camera(self): + return self._camera + @pyqtProperty(str, notify = typeChanged) def type(self): return self._type diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 8114a3eef5..6403bdf14d 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -12,6 +12,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp 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 @@ -290,6 +291,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if printer is None: printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) + printer.setCamera(NetworkCamera("http://" + printer_data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) printer_list_changed = True diff --git a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml index d0a9e08232..3e6f6a8fd8 100644 --- a/plugins/UM3NetworkPrinting/PrinterVideoStream.qml +++ b/plugins/UM3NetworkPrinting/PrinterVideoStream.qml @@ -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 ""; } From ba782b346f75a0d802a0a52920d0517b60f6e5a6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 6 Dec 2017 16:41:07 +0100 Subject: [PATCH 068/135] Fixed re-requesting authentication CL-541 --- .../UM3NetworkPrinting/LegacyUM3OutputDevice.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index d89efc2acc..8de18443a7 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -2,6 +2,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp 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 @@ -91,7 +92,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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._requestAuthentication) + 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")) @@ -102,7 +103,17 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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._requestAuthentication) + 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() + pass # Cura Connect doesn't do any authorization def connect(self): super().connect() @@ -530,6 +541,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): if not self._printers: self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders)] + self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream")) self.printersChanged.emit() # LegacyUM3 always has a single printer. From b3a3c1e371c7133d9f4a3f9e21271f60d304c182 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 6 Dec 2017 16:46:33 +0100 Subject: [PATCH 069/135] Camera image is now actually displayed for Legacy CL-541 --- .../LegacyUM3OutputDevice.py | 5 ++- plugins/UM3NetworkPrinting/MonitorItem.qml | 31 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 8de18443a7..5597e03d44 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -22,7 +22,7 @@ from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController from time import time import json - +import os i18n_catalog = i18nCatalog("cura") @@ -72,6 +72,9 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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): diff --git a/plugins/UM3NetworkPrinting/MonitorItem.qml b/plugins/UM3NetworkPrinting/MonitorItem.qml index f69df41dd4..09e427ff6f 100644 --- a/plugins/UM3NetworkPrinting/MonitorItem.qml +++ b/plugins/UM3NetworkPrinting/MonitorItem.qml @@ -9,35 +9,32 @@ Component Image { id: cameraImage - property bool proportionalHeight: - { - if(sourceSize.height == 0 || maximumHeight == 0) - { - return true; - } - 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) + 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 onVisibleChanged: { 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 ""; } From 73bae3754416a2c22e4dd3cd6e7e51f75af62261 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 6 Dec 2017 16:58:41 +0100 Subject: [PATCH 070/135] Added missing string to verification failed Message CL-541 --- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 5597e03d44..384e51bfce 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -91,7 +91,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): title=i18n_catalog.i18nc("@info:title", "Authentication status")) - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""), + self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Authentication failed"), 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")) From 4ba551a3af5b9ff0240cbccfab8a160d46164220 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 6 Dec 2017 17:02:35 +0100 Subject: [PATCH 071/135] Prevent crash when switching away from monitor tab CL-541 --- cura/PrinterOutput/NetworkCamera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py index 5cb76d2876..bffd318f41 100644 --- a/cura/PrinterOutput/NetworkCamera.py +++ b/cura/PrinterOutput/NetworkCamera.py @@ -59,7 +59,6 @@ class NetworkCamera(QObject): @pyqtSlot() def stop(self): - self._manager = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 @@ -80,6 +79,8 @@ class NetworkCamera(QObject): self._image_reply = None self._image_request = None + self._manager = None + self._started = False def getImage(self): From a8695db1c898ae26821ef8280d382a0be10a7507 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 8 Dec 2017 10:44:05 +0100 Subject: [PATCH 072/135] Fixed displaying of icons in monitor stage tab CL-541 --- plugins/MonitorStage/MonitorStage.py | 47 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 0736f49858..21d5bb6cde 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -40,34 +40,41 @@ class MonitorStage(CuraStage): ## 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.state == "maintenance": + return "tab_status_busy" + + if output_device.activePrinter.activeJob is None: + return "tab_status_connected" + + if output_device.activePrinter.activeJob.state in ["printing", "pre_print", "pausing", "resuming"]: + return "tab_status_busy" + + if output_device.activePrinter.activeJob.state == "wait_cleanup": + return "tab_status_finished" + + if output_device.activePrinter.activeJob.state in ["ready", ""]: + return "tab_status_connected" + + if output_device.activePrinter.activeJob.state == "paused": return "tab_status_paused" - if output_device.jobState == "error": + if output_device.activePrinter.activeJob.state == "error": return "tab_status_stopped" return "tab_status_unknown" From 9ccd643f64cceb47e500cdf72fbe2dc2dfe46000 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 8 Dec 2017 14:36:53 +0100 Subject: [PATCH 073/135] Repaired the monitor icon not being updated CL-541 --- cura/PrinterOutputDevice.py | 2 +- cura/Settings/MachineManager.py | 3 +- plugins/MonitorStage/MonitorMainView.qml | 1 - plugins/MonitorStage/MonitorStage.py | 88 +++++++++++++++---- .../LegacyUM3OutputDevice.py | 9 +- 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 2126e791d3..91b981f3b6 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -143,7 +143,7 @@ class PrinterOutputDevice(QObject, OutputDevice): if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands - self.acceptsCommandsChanged.emit() + self.acceptsCommandsChanged.emit() ## The current processing state of the backend. diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index c53fa15f1a..7920e89232 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -137,8 +137,7 @@ class MachineManager(QObject): printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged) printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)''' - self._printer_output_devices.clear() - + self._printer_output_devices = [] for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) diff --git a/plugins/MonitorStage/MonitorMainView.qml b/plugins/MonitorStage/MonitorMainView.qml index 038403e6d3..fad76cba30 100644 --- a/plugins/MonitorStage/MonitorMainView.qml +++ b/plugins/MonitorStage/MonitorMainView.qml @@ -16,7 +16,6 @@ Item color: UM.Theme.getColor("viewport_overlay") width: parent.width height: parent.height - visible: monitorViewComponent.sourceComponent == null ? 1 : 0 MouseArea { diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 21d5bb6cde..ad63e65943 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -14,26 +14,79 @@ 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_printer.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.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._setActivePrinter(self._printer_output_device.activePrinter) + + 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._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)) @@ -56,25 +109,22 @@ class MonitorStage(CuraStage): if output_device.activePrinter.state == "maintenance": return "tab_status_busy" - if output_device.state == "maintenance": - return "tab_status_busy" - - if output_device.activePrinter.activeJob is None: + if output_device.activePrinter.activePrintJob is None: return "tab_status_connected" - if output_device.activePrinter.activeJob.state in ["printing", "pre_print", "pausing", "resuming"]: + if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]: return "tab_status_busy" - if output_device.activePrinter.activeJob.state == "wait_cleanup": + if output_device.activePrinter.activePrintJob.state == "wait_cleanup": return "tab_status_finished" - if output_device.activePrinter.activeJob.state in ["ready", ""]: + if output_device.activePrinter.activePrintJob.state in ["ready", ""]: return "tab_status_connected" - if output_device.activePrinter.activeJob.state == "paused": + if output_device.activePrinter.activePrintJob.state == "paused": return "tab_status_paused" - if output_device.activePrinter.activeJob.state == "error": + if output_device.activePrinter.activePrintJob.state == "error": return "tab_status_stopped" return "tab_status_unknown" diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 384e51bfce..268debbf7c 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -91,7 +91,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): title=i18n_catalog.i18nc("@info:title", "Authentication status")) - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", "Authentication failed"), + 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")) @@ -352,7 +352,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): return warnings - def _update(self): if not super()._update(): return @@ -401,10 +400,12 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_id = None self._authentication_key = None self.setAuthenticationState(AuthState.NotAuthenticated) - elif status_code == 403: + 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", + "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", self._authentication_state) + print(reply.readAll()) self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() elif status_code == 200: From ae629e2968fbbd251c779db82a806e8432d4dd30 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 8 Dec 2017 14:59:14 +0100 Subject: [PATCH 074/135] Fixed camera for legacy UM3 printer again CL-541 --- cura/PrinterOutput/NetworkCamera.py | 1 - cura/PrinterOutputDevice.py | 1 - .../LegacyUM3OutputDevice.py | 1 - plugins/UM3NetworkPrinting/MonitorItem.qml | 48 ++++++++++++------- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py index bffd318f41..b81914ca7d 100644 --- a/cura/PrinterOutput/NetworkCamera.py +++ b/cura/PrinterOutput/NetworkCamera.py @@ -59,7 +59,6 @@ class NetworkCamera(QObject): @pyqtSlot() def stop(self): - self._stream_buffer = b"" self._stream_buffer_start_index = -1 diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 91b981f3b6..fdf9a77145 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -97,7 +97,6 @@ class PrinterOutputDevice(QObject, OutputDevice): # create the item (and fail) every time. if not self._monitor_component: self._createMonitorViewFromQML() - return self._monitor_item @pyqtProperty(QObject, constant=True) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 268debbf7c..c7fdf9bdc6 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -74,7 +74,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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): diff --git a/plugins/UM3NetworkPrinting/MonitorItem.qml b/plugins/UM3NetworkPrinting/MonitorItem.qml index 09e427ff6f..bbbc3feee6 100644 --- a/plugins/UM3NetworkPrinting/MonitorItem.qml +++ b/plugins/UM3NetworkPrinting/MonitorItem.qml @@ -6,37 +6,49 @@ import Cura 1.0 as Cura Component { - Image + Item { - 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 - onVisibleChanged: + width: maximumWidth + height: maximumHeight + Image { - if(visible) + 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: { if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) { OutputDevice.activePrinter.camera.start() } - } else + } + onVisibleChanged: { - if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) + if(visible) { - OutputDevice.activePrinter.camera.stop() + 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.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) + source: { - return OutputDevice.activePrinter.camera.latestImage; + if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) + { + return OutputDevice.activePrinter.camera.latestImage; + } + return ""; } - return ""; } } } \ No newline at end of file From 1719a7b2fe53fe14a2eb68724692e5390dd326b0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 8 Dec 2017 17:16:59 +0100 Subject: [PATCH 075/135] Fixed preheating for Legacy UM3 CL-541 --- cura/PrinterOutput/PrinterOutputModel.py | 20 ++++- plugins/MonitorStage/MonitorStage.py | 2 +- .../LegacyUM3OutputDevice.py | 12 ++- .../LegacyUM3PrinterOutputController.py | 63 ++++++++++++++++ resources/qml/PrinterOutput/HeatedBedBox.qml | 74 ++----------------- 5 files changed, 97 insertions(+), 74 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index aaf9b48968..0c30d8d788 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -16,6 +16,7 @@ if MYPY: class PrinterOutputModel(QObject): bedTemperatureChanged = pyqtSignal() targetBedTemperatureChanged = pyqtSignal() + isPreheatingChanged = pyqtSignal() stateChanged = pyqtSignal() activePrintJobChanged = pyqtSignal() nameChanged = pyqtSignal() @@ -24,7 +25,7 @@ class PrinterOutputModel(QObject): typeChanged = pyqtSignal() cameraChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None): + def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""): super().__init__(parent) self._bed_temperature = -1 # Use -1 for no heated bed. self._target_bed_temperature = 0 @@ -34,18 +35,31 @@ class PrinterOutputModel(QObject): self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)] self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] - + self._firmware_version = firmware_version self._printer_state = "unknown" - + self._is_preheating = False self._type = "" self._camera = None + @pyqtProperty(str, constant = True) + def firmwareVersion(self): + return self._firmware_version + def setCamera(self, camera): if self._camera is not camera: self._camera = camera self.cameraChanged.emit() + def updateIsPreheating(self, pre_heating): + if self._is_preheating != pre_heating: + self._is_preheating = pre_heating + self.isPreheatingChanged.emit() + + @pyqtProperty(bool, notify=isPreheatingChanged) + def isPreheating(self): + return self._is_preheating + @pyqtProperty(QObject, notify=cameraChanged) def camera(self): return self._camera diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index ad63e65943..f223ef1844 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -23,7 +23,7 @@ class MonitorStage(CuraStage): def _setActivePrintJob(self, print_job): if self._active_print_job != print_job: if self._active_print_job: - self._active_printer.stateChanged.disconnect(self._updateIconSource) + 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) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index c7fdf9bdc6..967c99995e 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -543,7 +543,9 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): return if not self._printers: - self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders)] + # 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")) self.printersChanged.emit() @@ -553,6 +555,14 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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"]) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py index 54c126e5cc..c476673353 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py @@ -2,6 +2,8 @@ # 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: @@ -12,11 +14,33 @@ if MYPY: 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 + # 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 @@ -27,3 +51,42 @@ class LegacyUM3PrinterOutputController(PrinterOutputController): 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 + + diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index 5f09160708..65c2a161bd 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -136,15 +136,6 @@ Item color: UM.Theme.getColor("setting_control_highlight") opacity: preheatTemperatureControl.hovered ? 1.0 : 0 } - Label //Maximum temperature indication. - { - text: (bedTemperature.properties.maximum_value != "None" ? bedTemperature.properties.maximum_value : "") + "°C" - color: UM.Theme.getColor("setting_unit") - font: UM.Theme.getFont("default") - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width - anchors.verticalCenter: parent.verticalCenter - } MouseArea //Change cursor on hovering. { id: preheatTemperatureInputMouseArea @@ -204,58 +195,6 @@ Item } } - UM.RecolorImage - { - id: preheatCountdownIcon - width: UM.Theme.getSize("save_button_specs_icons").width - height: UM.Theme.getSize("save_button_specs_icons").height - sourceSize.width: width - sourceSize.height: height - color: UM.Theme.getColor("text") - visible: preheatCountdown.visible - source: UM.Theme.getIcon("print_time") - anchors.right: preheatCountdown.left - anchors.rightMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) - anchors.verticalCenter: preheatCountdown.verticalCenter - } - - Timer - { - id: preheatUpdateTimer - interval: 100 //Update every 100ms. You want to update every 1s, but then you have one timer for the updating running out of sync with the actual date timer and you might skip seconds. - running: printerModel != null && printerModel.preheatBedRemainingTime != "" - repeat: true - onTriggered: update() - property var endTime: new Date() //Set initial endTime to be the current date, so that the endTime has initially already passed and the timer text becomes invisible if you were to update. - function update() - { - if(printerModel != null && !printerModel.canPreHeatBed) - { - return // Nothing to do, printer cant preheat at all! - } - preheatCountdown.text = "" - if (printerModel != null && connectedPrinter.preheatBedRemainingTime != null) - { - preheatCountdown.text = connectedPrinter.preheatBedRemainingTime; - } - if (preheatCountdown.text == "") //Either time elapsed or not connected. - { - stop(); - } - } - } - Label - { - id: preheatCountdown - text: printerModel != null ? printerModel.preheatBedRemainingTime : "" - visible: text != "" //Has no direct effect, but just so that we can link visibility of clock icon to visibility of the countdown text. - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - anchors.right: preheatButton.left - anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.verticalCenter: preheatButton.verticalCenter - } - Button //The pre-heat button. { id: preheatButton @@ -267,9 +206,9 @@ Item { return false; //Not connected, not authenticated or printer is busy. } - if (preheatUpdateTimer.running) + if (printerModel.isPreheating) { - return true; //Can always cancel if the timer is running. + return true; } if (bedTemperature.properties.minimum_value != "None" && Math.floor(preheatTemperatureInput.text) < Math.floor(bedTemperature.properties.minimum_value)) { @@ -363,23 +302,20 @@ Item } } font: UM.Theme.getFont("action_button") - text: preheatUpdateTimer.running ? catalog.i18nc("@button Cancel pre-heating", "Cancel") : catalog.i18nc("@button", "Pre-heat") + text: printerModel.isPreheating ? catalog.i18nc("@button Cancel pre-heating", "Cancel") : catalog.i18nc("@button", "Pre-heat") } } } onClicked: { - if (!preheatUpdateTimer.running) + if (!printerModel.isPreheating) { - printerModel.preheatBed(preheatTemperatureInput.text, printerModel.preheatBedTimeout); - preheatUpdateTimer.start(); - preheatUpdateTimer.update(); //Update once before the first timer is triggered. + printerModel.preheatBed(preheatTemperatureInput.text, 900); } else { printerModel.cancelPreheatBed(); - preheatUpdateTimer.update(); } } From 6ad82ee1b04b1e3c83faf6d3eba26d25fcbfdfd6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 8 Dec 2017 17:32:22 +0100 Subject: [PATCH 076/135] Added missing name & description for ClusterOutputDevice CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 6403bdf14d..ba82da64c3 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -62,6 +62,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._active_printer = None # type: Optional[PrinterOutputModel] + 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")) + @pyqtProperty(QObject, notify=activePrinterChanged) def controlItem(self): if self._active_printer is None: From 8bc9663294c7253f0bea512e16011b573dc47688 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 12 Dec 2017 10:28:55 +0100 Subject: [PATCH 077/135] Removed old & unused code CL-541 --- .../NetworkClusterPrinterOutputDevice.py | 716 --------- .../NetworkPrinterOutputDevice.py | 1306 ----------------- .../NetworkPrinterOutputDevicePlugin.py | 357 ----- plugins/UM3NetworkPrinting/__init__.py | 4 +- 4 files changed, 2 insertions(+), 2381 deletions(-) delete mode 100644 plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py delete mode 100755 plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py delete mode 100644 plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py deleted file mode 100644 index 853ef72f72..0000000000 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ /dev/null @@ -1,716 +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._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] - - 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.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 - require_printer_name = self._selected_printer["unique_name"] - - self._send_gcode_start = time.time() - Logger.log("d", "Sending print job [%s] to host..." % file_name) - - 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 - - self._file_name = "%s.gcode.gz" % file_name - self._showProgressMessage() - - new_request = self._buildSendPrintJobHttpRequest(require_printer_name) - 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 _finishedPostPrintJobRequest() - - def _buildSendPrintJobHttpRequest(self, require_printer_name): - 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) - - gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") - compressed_gcode = self._compressGcode(gcode) - 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): - 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 is None: - Logger.log("e", "Unable to find sliced gcode, returning empty.") - return byte_array_file_data - - for line in gcode: - 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() - - 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 {file_name} 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()) diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py deleted file mode 100755 index 3a48bab11b..0000000000 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ /dev/null @@ -1,1306 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from UM.i18n import i18nCatalog -from UM.Application import Application -from UM.Logger import Logger -from UM.Signal import signalemitter - -from UM.Message import Message - -import UM.Settings.ContainerRegistry -import UM.Version #To compare firmware version numbers. - -from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState -from cura.Settings.ContainerManager import ContainerManager -import cura.Settings.ExtruderManager - -from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtProperty, pyqtSlot, QCoreApplication -from PyQt5.QtGui import QImage, QColor -from PyQt5.QtWidgets import QMessageBox - -import json -import os -import gzip - -from time import time - -from time import gmtime -from enum import IntEnum - -i18n_catalog = i18nCatalog("cura") - -class AuthState(IntEnum): - NotAuthenticated = 1 - AuthenticationRequested = 2 - Authenticated = 3 - AuthenticationDenied = 4 - -## Network connected (wifi / lan) printer that uses the Ultimaker API -@signalemitter -class NetworkPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, key, address, properties, api_prefix): - super().__init__(key) - self._address = address - self._key = key - self._properties = properties # Properties dict as provided by zero conf - self._api_prefix = api_prefix - - self._gcode = None - self._print_finished = True # _print_finished == False means we're halfway in a print - self._write_finished = True # _write_finished == False means we're currently sending a G-code file - - self._use_gzip = True # Should we use g-zip compression before sending the data? - - # This holds the full JSON file that was received from the last request. - # The JSON looks like: - #{ - # "led": {"saturation": 0.0, "brightness": 100.0, "hue": 0.0}, - # "beep": {}, - # "network": { - # "wifi_networks": [], - # "ethernet": {"connected": true, "enabled": true}, - # "wifi": {"ssid": "xxxx", "connected": False, "enabled": False} - # }, - # "diagnostics": {}, - # "bed": {"temperature": {"target": 60.0, "current": 44.4}}, - # "heads": [{ - # "max_speed": {"z": 40.0, "y": 300.0, "x": 300.0}, - # "position": {"z": 20.0, "y": 6.0, "x": 180.0}, - # "fan": 0.0, - # "jerk": {"z": 0.4, "y": 20.0, "x": 20.0}, - # "extruders": [ - # { - # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, - # "active_material": {"guid": "xxxxxxx", "length_remaining": -1.0}, - # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "AA 0.4"} - # }, - # { - # "feeder": {"max_speed": 45.0, "jerk": 5.0, "acceleration": 3000.0}, - # "active_material": {"guid": "xxxx", "length_remaining": -1.0}, - # "hotend": {"temperature": {"target": 0.0, "current": 22.8}, "id": "BB 0.4"} - # } - # ], - # "acceleration": 3000.0 - # }], - # "status": "printing" - #} - - self._json_printer_state = {} - - ## Todo: Hardcoded value now; we should probably read this from the machine file. - ## It's okay to leave this for now, as this plugin is um3 only (and has 2 extruders by definition) - self._num_extruders = 2 - - # These are reinitialised here (from PrinterOutputDevice) to match the new _num_extruders - self._hotend_temperatures = [0] * self._num_extruders - self._target_hotend_temperatures = [0] * self._num_extruders - - self._material_ids = [""] * self._num_extruders - self._hotend_ids = [""] * self._num_extruders - self._target_bed_temperature = 0 - self._processing_preheat_requests = True - - self._can_control_manually = False - - self.setPriority(3) # Make sure the output device gets selected above local file output - self.setName(key) - 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._manager = None - - self._post_request = None - self._post_reply = None - self._post_multi_part = None - self._post_part = None - - self._material_multi_part = None - self._material_part = None - - self._progress_message = None - self._error_message = None - self._connection_message = None - - self._update_timer = QTimer() - self._update_timer.setInterval(2000) # TODO; Add preference for update interval - self._update_timer.setSingleShot(False) - self._update_timer.timeout.connect(self._update) - - self._camera_timer = QTimer() - self._camera_timer.setInterval(500) # Todo: Add preference for camera update interval - self._camera_timer.setSingleShot(False) - self._camera_timer.timeout.connect(self._updateCamera) - - self._image_request = None - self._image_reply = None - - self._use_stream = True - self._stream_buffer = b"" - self._stream_buffer_start_index = -1 - - self._camera_image_id = 0 - - 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) - self._authentication_request_active = False - - self._authentication_state = AuthState.NotAuthenticated - self._authentication_id = None - self._authentication_key = None - - 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", "Connection status")) - self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""), title = i18n_catalog.i18nc("@info:title", "Connection 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.requestAuthentication) - self._authentication_succeeded_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), title = i18n_catalog.i18nc("@info:title", "Connection 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", "Connection 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.requestAuthentication) - - self._camera_image = QImage() - - self._material_post_objects = {} - self._connection_state_before_timeout = None - - self._last_response_time = time() - self._last_request_time = None - self._response_timeout_time = 10 - self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. - self._recreate_network_manager_count = 1 - - self._send_gcode_start = time() # Time when the sending of the g-code started. - - self._last_command = "" - - self._compressing_print = False - self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") - printer_type = self._properties.get(b"machine", b"").decode("utf-8") - if printer_type.startswith("9511"): - self._updatePrinterType("ultimaker3_extended") - elif printer_type.startswith("9066"): - self._updatePrinterType("ultimaker3") - else: - self._updatePrinterType("unknown") - - Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) - - def _onNetworkAccesibleChanged(self, accessible): - Logger.log("d", "Network accessible state changed to: %s", accessible) - - ## Triggered when the output device manager changes devices. - # - # This is how we can detect that our device is no longer active now. - def _onOutputDevicesChanged(self): - if self.getId() not in Application.getInstance().getOutputDeviceManager().getOutputDeviceIds(): - self.stopCamera() - - 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._key) - self.setAuthenticationState(AuthState.AuthenticationDenied) - - 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._key, 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._key) - - def getProperties(self): - return self._properties - - @pyqtSlot(str, result = str) - def getProperty(self, key): - key = key.encode("utf-8") - if key in self._properties: - return self._properties.get(key, b"").decode("utf-8") - else: - return "" - - ## Get the unique key of this machine - # \return key String containing the key of the machine. - @pyqtSlot(result = str) - def getKey(self): - return self._key - - ## The IP address of the printer. - @pyqtProperty(str, constant = True) - def address(self): - return self._properties.get(b"address", b"").decode("utf-8") - - ## Name of the printer (as returned from the ZeroConf properties) - @pyqtProperty(str, constant = True) - def name(self): - return self._properties.get(b"name", b"").decode("utf-8") - - ## Firmware version (as returned from the ZeroConf properties) - @pyqtProperty(str, constant=True) - def firmwareVersion(self): - return self._properties.get(b"firmware_version", b"").decode("utf-8") - - ## IPadress of this printer - @pyqtProperty(str, constant=True) - def ipAddress(self): - return self._address - - ## Pre-heats the heated bed of the printer. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. - @pyqtSlot(float, float) - def preheatBed(self, temperature, duration): - temperature = round(temperature) #The API doesn't allow floating point. - duration = round(duration) - if UM.Version.Version(self.firmwareVersion) < UM.Version.Version("3.5.92"): #Real bed pre-heating support is implemented from 3.5.92 and up. - self.setTargetBedTemperature(temperature = temperature) #No firmware-side duration support then. - return - url = QUrl("http://" + self._address + self._api_prefix + "printer/bed/pre_heat") - if duration > 0: - data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration) - else: - data = """{"temperature": "%i"}""" % temperature - Logger.log("i", "Pre-heating bed to %i degrees.", temperature) - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._processing_preheat_requests = False - self._manager.put(put_request, data.encode()) - self._preheat_bed_timer.start(self._preheat_bed_timeout * 1000) #Times 1000 because it needs to be provided as milliseconds. - self.preheatBedRemainingTimeChanged.emit() - - ## Cancels pre-heating the heated bed of the printer. - # - # If the bed is not pre-heated, nothing happens. - @pyqtSlot() - def cancelPreheatBed(self): - Logger.log("i", "Cancelling pre-heating of the bed.") - self.preheatBed(temperature = 0, duration = 0) - self._preheat_bed_timer.stop() - self._preheat_bed_timer.setInterval(0) - self.preheatBedRemainingTimeChanged.emit() - - ## Changes the target bed temperature on the printer. - # - # /param temperature The new target temperature of the bed. - def _setTargetBedTemperature(self, temperature): - if not self._updateTargetBedTemperature(temperature): - return - - url = QUrl("http://" + self._address + self._api_prefix + "printer/bed/temperature/target") - data = str(temperature) - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._manager.put(put_request, data.encode()) - - ## Updates the target bed temperature from the printer, and emit a signal if it was changed. - # - # /param temperature The new target temperature of the bed. - # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature - def _updateTargetBedTemperature(self, temperature): - if self._target_bed_temperature == temperature: - return False - self._target_bed_temperature = temperature - self.targetBedTemperatureChanged.emit() - return True - - ## Updates the target hotend temperature from the printer, and emit a signal if it was changed. - # - # /param index The index of the hotend. - # /param temperature The new target temperature of the hotend. - # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature - def _updateTargetHotendTemperature(self, index, temperature): - if self._target_hotend_temperatures[index] == temperature: - return False - self._target_hotend_temperatures[index] = temperature - self.targetHotendTemperaturesChanged.emit() - return True - - def _stopCamera(self): - self._stream_buffer = b"" - self._stream_buffer_start_index = -1 - - if self._camera_timer.isActive(): - self._camera_timer.stop() - - if self._image_reply: - try: - # disconnect the signal - try: - self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) - except Exception: - pass - # abort the request if it's not finished - if not self._image_reply.isFinished(): - self._image_reply.close() - except Exception as e: #RuntimeError - pass # It can happen that the wrapped c++ object is already deleted. - self._image_reply = None - self._image_request = None - - def _startCamera(self): - if self._use_stream: - self._startCameraStream() - else: - self._camera_timer.start() - - def _startCameraStream(self): - ## Request new image - url = QUrl("http://" + self._address + ":8080/?action=stream") - self._image_request = QNetworkRequest(url) - self._image_reply = self._manager.get(self._image_request) - self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) - - def _updateCamera(self): - if not self._manager.networkAccessible(): - return - ## Request new image - url = QUrl("http://" + self._address + ":8080/?action=snapshot") - image_request = QNetworkRequest(url) - self._manager.get(image_request) - self._last_request_time = time() - - ## Set the authentication state. - # \param auth_state \type{AuthState} Enum value representing the new auth state - def setAuthenticationState(self, auth_state): - if auth_state == self._authentication_state: - return # Nothing to do here. - - Logger.log("d", "Attempting to update auth state from %s to %s for printer %s" % (self._authentication_state, auth_state, self._key)) - - if auth_state == AuthState.AuthenticationRequested: - Logger.log("d", "Authentication state changed to authentication requested.") - self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. Please approve the access request on the printer.")) - self._authentication_requested_message.show() - self._authentication_request_active = True - self._authentication_timer.start() # Start timer so auth will fail after a while. - elif auth_state == AuthState.Authenticated: - Logger.log("d", "Authentication state changed to authenticated") - self.setAcceptsCommands(True) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network.")) - self._authentication_requested_message.hide() - if self._authentication_request_active: - self._authentication_succeeded_message.show() - - # Stop waiting for a response - self._authentication_timer.stop() - self._authentication_counter = 0 - - # Once we are authenticated we need to send all material profiles. - self.sendMaterialProfiles() - elif auth_state == AuthState.AuthenticationDenied: - self.setAcceptsCommands(False) - self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer.")) - self._authentication_requested_message.hide() - if self._authentication_request_active: - if self._authentication_timer.remainingTime() > 0: - Logger.log("d", "Authentication state changed to authentication denied before the request timeout.") - self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request was denied on the printer.")) - else: - Logger.log("d", "Authentication state changed to authentication denied due to a timeout") - self._authentication_failed_message.setText(i18n_catalog.i18nc("@info:status", "Access request failed due to a timeout.")) - - self._authentication_failed_message.show() - self._authentication_request_active = False - - # Stop waiting for a response - self._authentication_timer.stop() - self._authentication_counter = 0 - - self._authentication_state = auth_state - self.authenticationStateChanged.emit() - - authenticationStateChanged = pyqtSignal() - - @pyqtProperty(int, notify = authenticationStateChanged) - def authenticationState(self): - return self._authentication_state - - @pyqtSlot() - def requestAuthentication(self, message_id = None, action_id = "Retry"): - if action_id == "Request" or action_id == "Retry": - Logger.log("d", "Requestion authentication for %s due to action %s" % (self._key, action_id)) - self._authentication_failed_message.hide() - self._not_authenticated_message.hide() - self.setAuthenticationState(AuthState.NotAuthenticated) - self._authentication_counter = 0 - self._authentication_requested_message.setProgress(0) - self._authentication_id = None - self._authentication_key = None - self._createNetworkManager() # Re-create network manager to force re-authentication. - - ## Request data from the connected device. - def _update(self): - if self._last_response_time: - time_since_last_response = time() - self._last_response_time - else: - time_since_last_response = 0 - if self._last_request_time: - time_since_last_request = time() - self._last_request_time - else: - time_since_last_request = float("inf") # An irrelevantly large number of seconds - - # Connection is in timeout, check if we need to re-start the connection. - # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. - # Re-creating the QNetworkManager seems to fix this issue. - if self._last_response_time and self._connection_state_before_timeout: - if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: - self._recreate_network_manager_count += 1 - counter = 0 # Counter to prevent possible indefinite while loop. - # It can happen that we had a very long timeout (multiple times the recreate time). - # In that case we should jump through the point that the next update won't be right away. - while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time and counter < 10: - counter += 1 - self._recreate_network_manager_count += 1 - Logger.log("d", "Timeout lasted over %.0f seconds (%.1fs), re-checking connection.", self._recreate_network_manager_time, time_since_last_response) - self._createNetworkManager() - return - - # Check if we have an connection in the first place. - if not self._manager.networkAccessible(): - if not self._connection_state_before_timeout: - Logger.log("d", "The network connection seems to be disabled. Going into timeout mode") - self._connection_state_before_timeout = self._connection_state - self.setConnectionState(ConnectionState.error) - self._connection_message = Message(i18n_catalog.i18nc("@info:status", - "The connection with the network was lost."), - title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._connection_message.show() - - if self._progress_message: - self._progress_message.hide() - - # Check if we were uploading something. Abort if this is the case. - # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - Logger.log("d", "Stopping post upload because the connection was lost.") - self._finalizePostReply() - return - else: - if not self._connection_state_before_timeout: - self._recreate_network_manager_count = 1 - - # Check that we aren't in a timeout state - if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: - if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: - # Go into timeout state. - Logger.log("d", "We did not receive a response for %0.1f seconds, so it seems the printer is no longer accessible.", time_since_last_response) - self._connection_state_before_timeout = self._connection_state - self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with the printer was lost. Check your printer to see if it is connected."), - title = i18n_catalog.i18nc("@info:title", "Connection Status")) - self._connection_message.show() - - if self._progress_message: - self._progress_message.hide() - - # Check if we were uploading something. Abort if this is the case. - # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - Logger.log("d", "Stopping post upload because the connection was lost.") - self._finalizePostReply() - self.setConnectionState(ConnectionState.error) - return - - if self._authentication_state == AuthState.NotAuthenticated: - self._verifyAuthentication() # We don't know if we are authenticated; check if we have correct auth. - elif self._authentication_state == AuthState.AuthenticationRequested: - self._checkAuthentication() # We requested authentication at some point. Check if we got permission. - - ## Request 'general' printer data - url = QUrl("http://" + self._address + self._api_prefix + "printer") - printer_request = QNetworkRequest(url) - self._manager.get(printer_request) - - ## Request print_job data - url = QUrl("http://" + self._address + self._api_prefix + "print_job") - print_job_request = QNetworkRequest(url) - self._manager.get(print_job_request) - - self._last_request_time = time() - - def _finalizePostReply(self): - # Indicate uploading was finished (so another file can be send) - self._write_finished = True - - if self._post_reply is None: - return - - try: - try: - self._post_reply.uploadProgress.disconnect(self._onUploadProgress) - except TypeError: - pass # The disconnection can fail on mac in some cases. Ignore that. - - try: - self._post_reply.finished.disconnect(self._onUploadFinished) - except TypeError: - pass # The disconnection can fail on mac in some cases. Ignore that. - - self._post_reply.abort() - self._post_reply = None - except RuntimeError: - self._post_reply = None # It can happen that the wrapped c++ object is already deleted. - - def _createNetworkManager(self): - if self._manager: - self._manager.finished.disconnect(self._onFinished) - self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) - self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) - - self._manager = QNetworkAccessManager() - self._manager.finished.connect(self._onFinished) - self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes - - ## Convenience function that gets information from the received json data and converts it to the right internal - # values / variables - def _spliceJSONData(self): - # Check for hotend temperatures - for index in range(0, self._num_extruders): - temperatures = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["temperature"] - self._setHotendTemperature(index, temperatures["current"]) - self._updateTargetHotendTemperature(index, temperatures["target"]) - try: - material_id = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] - except KeyError: - material_id = "" - self._setMaterialId(index, material_id) - try: - hotend_id = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] - except KeyError: - hotend_id = "" - self._setHotendId(index, hotend_id) - - bed_temperatures = self._json_printer_state["bed"]["temperature"] - self._setBedTemperature(bed_temperatures["current"]) - self._updateTargetBedTemperature(bed_temperatures["target"]) - - head_x = self._json_printer_state["heads"][0]["position"]["x"] - head_y = self._json_printer_state["heads"][0]["position"]["y"] - head_z = self._json_printer_state["heads"][0]["position"]["z"] - self._updateHeadPosition(head_x, head_y, head_z) - self._updatePrinterState(self._json_printer_state["status"]) - - if self._processing_preheat_requests: - try: - is_preheating = self._json_printer_state["bed"]["pre_heat"]["active"] - except KeyError: #Old firmware doesn't support that. - pass #Don't update the pre-heat remaining time. - else: - if is_preheating: - try: - remaining_preheat_time = self._json_printer_state["bed"]["pre_heat"]["remaining"] - except KeyError: #Error in firmware. If "active" is supported, "remaining" should also be supported. - pass #Anyway, don't update. - else: - #Only update if time estimate is significantly off (>5000ms). - #Otherwise we get issues with latency causing the timer to count inconsistently. - if abs(self._preheat_bed_timer.remainingTime() - remaining_preheat_time * 1000) > 5000: - self._preheat_bed_timer.setInterval(remaining_preheat_time * 1000) - self._preheat_bed_timer.start() - self.preheatBedRemainingTimeChanged.emit() - else: #Not pre-heating. Must've cancelled. - if self._preheat_bed_timer.isActive(): - self._preheat_bed_timer.setInterval(0) - self._preheat_bed_timer.stop() - self.preheatBedRemainingTimeChanged.emit() - - def close(self): - Logger.log("d", "Closing connection of printer %s with ip %s", self._key, self._address) - self._updateJobState("") - self.setConnectionState(ConnectionState.closed) - if self._progress_message: - self._progress_message.hide() - - # Reset authentication state - self._authentication_requested_message.hide() - self.setAuthenticationState(AuthState.NotAuthenticated) - self._authentication_counter = 0 - self._authentication_timer.stop() - - self._authentication_requested_message.hide() - self._authentication_failed_message.hide() - self._authentication_succeeded_message.hide() - - # Reset stored material & hotend data. - self._material_ids = [""] * self._num_extruders - self._hotend_ids = [""] * self._num_extruders - - if self._error_message: - self._error_message.hide() - - # Reset timeout state - self._connection_state_before_timeout = None - self._last_response_time = time() - self._last_request_time = None - - # Stop update timers - self._update_timer.stop() - - self.stopCamera() - - ## Request the current scene to be sent to a network-connected printer. - # - # \param nodes A collection of scene nodes to send. This is ignored. - # \param file_name \type{string} A suggestion for a file name to write. - # This is ignored. - # \param filter_by_machine Whether to filter MIME types by machine. This - # is ignored. - # \param kwargs Keyword arguments. - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): - - if self._printer_state not in ["idle", ""]: - self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._printer_state, - title = i18n_catalog.i18nc("@info:title", "Printer Status")) - self._error_message.show() - return - elif self._authentication_state != AuthState.Authenticated: - self._not_authenticated_message.show() - Logger.log("d", "Attempting to perform an action without authentication for printer %s. Auth state is %s", self._key, self._authentication_state) - return - - Application.getInstance().getController().setActiveStage("MonitorStage") - self._print_finished = True - self.writeStarted.emit(self) - self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") - - print_information = Application.getInstance().getPrintInformation() - warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about. - - # Only check for mistakes if there is material length information. - if print_information.materialLengths: - # Check if PrintCores / materials are loaded at all. Any failure in these results in an Error. - for index in range(0, self._num_extruders): - if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: - if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "": - Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1) - self._error_message = Message( - i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No Printcore loaded in slot {0}".format(index + 1)), - title = i18n_catalog.i18nc("@info:title", "Error")) - self._error_message.show() - return - if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "": - Logger.log("e", "No material loaded in slot %s, unable to start print", index + 1) - self._error_message = Message( - i18n_catalog.i18nc("@info:status", - "Unable to start a new print job. No material loaded in slot {0}".format(index + 1)), - title = i18n_catalog.i18nc("@info:title", "Error")) - self._error_message.show() - return - - for index in range(0, self._num_extruders): - # Check if there is enough material. Any failure in these results in a warning. - material_length = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["length_remaining"] - if material_length != -1 and index < len(print_information.materialLengths) and print_information.materialLengths[index] > material_length: - Logger.log("w", "Printer reports that there is not enough material left for extruder %s. We need %s and the printer has %s", index + 1, print_information.materialLengths[index], material_length) - warnings.append(i18n_catalog.i18nc("@label", "Not enough material for spool {0}.").format(index+1)) - - # Check if the right cartridges are loaded. Any failure in these results in a warning. - extruder_manager = cura.Settings.ExtruderManager.ExtruderManager.getInstance() - if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: - variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) - core_name = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] - if variant: - if variant.getName() != core_name: - Logger.log("w", "Extruder %s has a different Cartridge (%s) as Cura (%s)", index + 1, core_name, variant.getName()) - warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {0}, Printer: {1}) selected for extruder {2}".format(variant.getName(), core_name, index + 1))) - - material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) - if material: - remote_material_guid = self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] - if material.getMetaDataEntry("GUID") != remote_material_guid: - Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, - remote_material_guid, - material.getMetaDataEntry("GUID")) - - remote_materials = UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(type = "material", GUID = remote_material_guid, read_only = True) - remote_material_name = "Unknown" - if remote_materials: - remote_material_name = remote_materials[0].getName() - warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(material.getName(), remote_material_name, index + 1)) - - try: - is_offset_calibrated = self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["offset"]["state"] == "valid" - except KeyError: # Older versions of the API don't expose the offset property, so we must asume that all is well. - is_offset_calibrated = True - - if not is_offset_calibrated: - warnings.append(i18n_catalog.i18nc("@label", "PrintCore {0} is not properly calibrated. XY calibration needs to be performed on the printer.").format(index + 1)) - else: - Logger.log("w", "There was no material usage found. No check to match used material with machine is done.") - - 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._configurationMismatchMessageCallback - ) - return - - self.startPrint() - - def _configurationMismatchMessageCallback(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 isConnected(self): - return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error - - ## Start requesting data from printer - def connect(self): - # Don't allow to connect to a printer with a faulty connection state. - # For instance when switching printers but the printer is disconnected from the network - if self._connection_state == ConnectionState.error: - return - - if self.isConnected(): - self.close() # Close previous connection - - self._createNetworkManager() - - self._last_response_time = time() # Ensure we reset the time when trying to connect (again) - - self.setConnectionState(ConnectionState.connecting) - self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. - if not self._use_stream: - self._updateCamera() - Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) - - ## Check if this machine was authenticated before. - self._authentication_id = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_id", None) - self._authentication_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("network_authentication_key", None) - - if self._authentication_id is None and self._authentication_key is None: - Logger.log("d", "No authentication found in metadata.") - else: - Logger.log("d", "Loaded authentication id %s and key %s from the metadata entry for printer %s", self._authentication_id, self._getSafeAuthKey(), self._key) - - self._update_timer.start() - - ## Stop requesting data from printer - def disconnect(self): - Logger.log("d", "Connection with printer %s with ip %s stopped", self._key, self._address) - self.close() - - newImage = pyqtSignal() - - @pyqtProperty(QUrl, notify = newImage) - def cameraImage(self): - self._camera_image_id += 1 - # There is an image provider that is called "camera". In order to ensure that the image qml object, that - # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl - # as new (instead of relying on cached version and thus forces an update. - temp = "image://camera/" + str(self._camera_image_id) - return QUrl(temp, QUrl.TolerantMode) - - def getCameraImage(self): - return self._camera_image - - def _setJobState(self, job_state): - self._last_command = job_state - url = QUrl("http://" + self._address + self._api_prefix + "print_job/state") - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - data = "{\"target\": \"%s\"}" % job_state - self._manager.put(put_request, data.encode()) - - ## Convenience function to get the username from the OS. - # The code was copied from the getpass module, as we try to use as little dependencies as possible. - def _getUserName(self): - for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): - user = os.environ.get(name) - if user: - return user - return "Unknown User" # Couldn't find out username. - - def _progressMessageActionTrigger(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_print = False - self._write_finished = True # post_reply does not always exist, so make sure we unblock writing - if self._post_reply: - self._finalizePostReply() - Application.getInstance().getController().setActiveStage("PrepareStage") - - ## Attempt to start a new print. - # This function can fail to actually start a print due to not being authenticated or another print already - # being in progress. - def startPrint(self): - - # Check if we're already writing - if not self._write_finished: - 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 - - # Indicate we're starting a new write action, is set back to True at the end of this method - self._write_finished = False - - try: - 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._progressMessageActionTrigger) - self._progress_message.show() - Logger.log("d", "Started sending g-code to remote printer.") - self._compressing_print = True - ## Mash the data into single string - - max_chars_per_line = 1024 * 1024 / 4 # 1 / 4 MB - - byte_array_file_data = b"" - batched_line = "" - - def _compress_data_and_notify_qt(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() - return compressed_data - - for line in self._gcode: - if not self._compressing_print: - self._progress_message.hide() - return # Stop trying to zip, abort was called. - - if self._use_gzip: - 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 += _compress_data_and_notify_qt(batched_line) - batched_line = "" - else: - byte_array_file_data += line.encode("utf-8") - - # don't miss the last batch if it's there - if self._use_gzip: - if batched_line: - byte_array_file_data += _compress_data_and_notify_qt(batched_line) - - if self._use_gzip: - file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName - else: - file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName - - self._compressing_print = False - ## Create multi_part request - self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) - - ## Create part (to be placed inside multipart) - self._post_part = QHttpPart() - self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, - "form-data; name=\"file\"; filename=\"%s\"" % file_name) - self._post_part.setBody(byte_array_file_data) - self._post_multi_part.append(self._post_part) - - url = QUrl("http://" + self._address + self._api_prefix + "print_job") - - ## Create the QT request - self._post_request = QNetworkRequest(url) - - ## Post request + data - self._post_reply = self._manager.post(self._post_request, self._post_multi_part) - self._post_reply.uploadProgress.connect(self._onUploadProgress) - self._post_reply.finished.connect(self._onUploadFinished) # used to unblock new write actions - - except IOError: - self._progress_message.hide() - self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?"), - title = i18n_catalog.i18nc("@info:title", "Warning")) - self._error_message.show() - except Exception as e: - self._progress_message.hide() - Logger.log("e", "An exception occurred in network connection: %s" % str(e)) - - ## Verify if we are authenticated to make requests. - def _verifyAuthentication(self): - url = QUrl("http://" + self._address + self._api_prefix + "auth/verify") - request = QNetworkRequest(url) - self._manager.get(request) - - ## Check if the authentication request was allowed by the printer. - def _checkAuthentication(self): - Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - self._manager.get(QNetworkRequest(QUrl("http://" + self._address + self._api_prefix + "auth/check/" + str(self._authentication_id)))) - - ## Request a authentication key from the printer so we can be authenticated - def _requestAuthentication(self): - url = QUrl("http://" + self._address + self._api_prefix + "auth/request") - request = QNetworkRequest(url) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._authentication_key = None - self._authentication_id = None - self._manager.post(request, json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode()) - self.setAuthenticationState(AuthState.AuthenticationRequested) - - ## Send all material profiles to the printer. - def sendMaterialProfiles(self): - for container in UM.Settings.ContainerRegistry.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. - - material_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) - - material_part = QHttpPart() - file_name = "none.xml" - material_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\";filename=\"%s\"" % file_name) - material_part.setBody(xml_data.encode()) - material_multi_part.append(material_part) - url = QUrl("http://" + self._address + self._api_prefix + "materials") - material_post_request = QNetworkRequest(url) - reply = self._manager.post(material_post_request, material_multi_part) - - # Keep reference to material_part, material_multi_part and reply so the garbage collector won't touch them. - self._material_post_objects[id(reply)] = (material_part, material_multi_part, reply) - 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 - - ## Handler for all requests that have finished. - def _onFinished(self, reply): - if reply.error() == QNetworkReply.TimeoutError: - Logger.log("w", "Received a timeout on a request to the printer") - self._connection_state_before_timeout = self._connection_state - # Check if we were uploading something. Abort if this is the case. - # Some operating systems handle this themselves, others give weird issues. - if self._post_reply: - self._finalizePostReply() - Logger.log("d", "Uploading of print failed after %s", time() - self._send_gcode_start) - self._progress_message.hide() - - self.setConnectionState(ConnectionState.error) - return - - if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. - Logger.log("d", "We got a response (%s) from the server after %0.1f of silence. Going back to previous state %s", reply.url().toString(), time() - self._last_response_time, self._connection_state_before_timeout) - - # Camera was active before timeout. Start it again - if self._camera_active: - self._startCamera() - - self.setConnectionState(self._connection_state_before_timeout) - self._connection_state_before_timeout = None - - if reply.error() == QNetworkReply.NoError: - self._last_response_time = time() - - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if not status_code: - if self._connection_state != ConnectionState.error: - Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) - # Received no or empty reply - return - reply_url = reply.url().toString() - - if reply.operation() == QNetworkAccessManager.GetOperation: - # "printer" is also in "printers", therefore _api_prefix is added. - if self._api_prefix + "printer" in reply_url: # Status update from printer. - if status_code == 200: - if self._connection_state == ConnectionState.connecting: - self.setConnectionState(ConnectionState.connected) - try: - self._json_printer_state = 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 - self._spliceJSONData() - - # Hide connection error message if the connection was restored - if self._connection_message: - self._connection_message.hide() - self._connection_message = None - else: - Logger.log("w", "We got an unexpected status (%s) while requesting printer state", status_code) - pass # TODO: Handle errors - elif self._api_prefix + "print_job" in reply_url: # Status update from print_job: - if status_code == 200: - 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 - progress = json_data["progress"] - ## If progress is 0 add a bit so another print can't be sent. - if progress == 0: - progress += 0.001 - elif progress == 1: - self._print_finished = True - else: - self._print_finished = False - self.setProgress(progress * 100) - - state = json_data["state"] - - # There is a short period after aborting or finishing a print where the printer - # reports a "none" state (but the printer is not ready to receive a print) - # If this happens before the print has reached progress == 1, the print has - # been aborted. - if state == "none" or state == "": - if self._last_command == "abort": - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Aborting print...")) - state = "error" - else: - state = "printing" - if state == "wait_cleanup" and self._last_command == "abort": - # Keep showing the "aborted" error state until after the buildplate has been cleaned - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Print aborted. Please check the printer")) - state = "error" - - # NB/TODO: the following two states are intentionally added for future proofing the i18n strings - # but are currently non-functional - if state == "!pausing": - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Pausing print...")) - if state == "!resuming": - self.setErrorText(i18n_catalog.i18nc("@label:MonitorStatus", "Resuming print...")) - - self._updateJobState(state) - self.setTimeElapsed(json_data["time_elapsed"]) - self.setTimeTotal(json_data["time_total"]) - self.setJobName(json_data["name"]) - elif status_code == 404: - self.setProgress(0) # No print job found, so there can't be progress or other data. - self._updateJobState("") - self.setErrorText("") - self.setTimeElapsed(0) - self.setTimeTotal(0) - self.setJobName("") - else: - Logger.log("w", "We got an unexpected status (%s) while requesting print job state", status_code) - elif "snapshot" in reply_url: # Status update from image: - if status_code == 200: - self._camera_image.loadFromData(reply.readAll()) - self.newImage.emit() - elif "auth/verify" in reply_url: # Answer when requesting authentication - if status_code == 401: - if self._authentication_state != AuthState.AuthenticationRequested: - # Only request a new authentication when we have not already done so. - Logger.log("i", "Not authenticated (Current auth state is %s). Attempting to request authentication for printer %s", self._authentication_state, self._key ) - self._requestAuthentication() - elif status_code == 403: - # If we already had an auth (eg; didn't request one), we only need a single 403 to see it as denied. - if self._authentication_state != AuthState.AuthenticationRequested: - 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) - elif status_code == 200: - self.setAuthenticationState(AuthState.Authenticated) - global_container_stack = Application.getInstance().getGlobalContainerStack() - - ## Save authentication details. - 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) - Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - Application.getInstance().saveStack(global_container_stack) # Force save so we are sure the data is not lost. - else: - Logger.log("w", "Unable to save authentication for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) - - # Request 'system' printer data once, when we know we have authentication, so we know we can set the system time. - url = QUrl("http://" + self._address + self._api_prefix + "system") - system_data_request = QNetworkRequest(url) - self._manager.get(system_data_request) - - else: # Got a response that we didn't expect, so something went wrong. - Logger.log("e", "While trying to authenticate, we got an unexpected response: %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)) - self.setAuthenticationState(AuthState.NotAuthenticated) - - elif "auth/check" in reply_url: # Check if we are authenticated (user can refuse this!) - 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._verifyAuthentication() # Ensure that the verification is really used and correct. - elif data.get("message", "") == "unauthorized": - Logger.log("i", "Authentication was denied.") - self.setAuthenticationState(AuthState.AuthenticationDenied) - else: - pass - - elif self._api_prefix + "system" in reply_url: - # Check if the printer has time, and if this has a valid system time. - 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.") - return - if "time" in data and "utc" in data["time"]: - try: - printer_time = gmtime(float(data["time"]["utc"])) - Logger.log("i", "Printer has system time of: %s", str(printer_time)) - except ValueError: - printer_time = None - if printer_time is not None and printer_time.tm_year < 1990: - # The system time is not valid, sync our current system time to it, so we at least have some reasonable time in the printer. - Logger.log("w", "Printer system time invalid, setting system time") - url = QUrl("http://" + self._address + self._api_prefix + "system/time/utc") - put_request = QNetworkRequest(url) - put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self._manager.put(put_request, str(time()).encode()) - - elif reply.operation() == QNetworkAccessManager.PostOperation: - if "/auth/request" in reply_url: - # We got a response to requesting authentication. - 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.") - return - global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack: # Remove any old data. - Logger.log("d", "Removing old network authentication data for %s as a new one was requested.", self._key) - global_container_stack.removeMetaDataEntry("network_authentication_key") - global_container_stack.removeMetaDataEntry("network_authentication_id") - Application.getInstance().saveStack(global_container_stack) # Force saving so we don't keep wrong auth data. - - self._authentication_key = data["key"] - self._authentication_id = data["id"] - Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey()) - - # Check if the authentication is accepted. - self._checkAuthentication() - elif "materials" in reply_url: - # Remove cached post request items. - del self._material_post_objects[id(reply)] - elif "print_job" in reply_url: - self._onUploadFinished() # Make sure the upload flag is reset as reply.finished is not always triggered - try: - reply.uploadProgress.disconnect(self._onUploadProgress) - except: - pass - try: - reply.finished.disconnect(self._onUploadFinished) - except: - pass - Logger.log("d", "Uploading of print succeeded after %s", time() - self._send_gcode_start) - # Only reset the _post_reply if it was the same one. - if reply == self._post_reply: - self._post_reply = None - self._progress_message.hide() - - elif reply.operation() == QNetworkAccessManager.PutOperation: - if "printer/bed/pre_heat" in reply_url: #Pre-heat command has completed. Re-enable syncing pre-heating. - self._processing_preheat_requests = True - if status_code in [200, 201, 202, 204]: - pass # Request was successful! - else: - Logger.log("d", "Something went wrong when trying to update data of API (%s). Message: %s Statuscode: %s", reply_url, reply.readAll(), status_code) - else: - Logger.log("d", "NetworkPrinterOutputDevice got an unhandled operation %s", reply.operation()) - - def _onStreamDownloadProgress(self, bytes_received, bytes_total): - # An MJPG stream is (for our purpose) a stream of concatenated JPG images. - # JPG images start with the marker 0xFFD8, and end with 0xFFD9 - if self._image_reply is None: - return - self._stream_buffer += self._image_reply.readAll() - - if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger - Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...") - self._stopCamera() # resets stream buffer and start index - self._startCamera() - return - - if self._stream_buffer_start_index == -1: - self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8') - stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9') - # If this happens to be more than a single frame, then so be it; the JPG decoder will - # ignore the extra data. We do it like this in order not to get a buildup of frames - - if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1: - jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2] - self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:] - self._stream_buffer_start_index = -1 - - self._camera_image.loadFromData(jpg_data) - self.newImage.emit() - - def _onUploadProgress(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() - - ## Allow new write actions (uploads) again when uploading is finished. - def _onUploadFinished(self): - self._write_finished = True - - ## Let the user decide if the hotends and/or material should be synced with the printer - 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 - ) - - ## 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 diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py deleted file mode 100644 index 46538f1af9..0000000000 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevicePlugin.py +++ /dev/null @@ -1,357 +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 = [] - - # 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)) - 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())) diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py index 6dd86a16d2..b68086cb75 100644 --- a/plugins/UM3NetworkPrinting/__init__.py +++ b/plugins/UM3NetworkPrinting/__init__.py @@ -1,6 +1,6 @@ -# 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 b61832bc03b6367b18d8182c4e10fd51275317b5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 12 Dec 2017 10:41:27 +0100 Subject: [PATCH 078/135] Added manual entry to prevent jogging for UM3 I've not had the time to properly build it yet, so disabling it for now CL-541 --- plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py index c476673353..7a0e113d5b 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3PrinterOutputController.py @@ -18,6 +18,9 @@ class LegacyUM3PrinterOutputController(PrinterOutputController): 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 From 005ba4ac5300d86a86fd86a1464bd5d645928cb5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 14 Dec 2017 09:34:16 +0100 Subject: [PATCH 079/135] Changed hotend properties to float CL-541 --- cura/PrinterOutput/ExtruderOuputModel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOuputModel.py index f8f8088389..b0be6cbbe4 100644 --- a/cura/PrinterOutput/ExtruderOuputModel.py +++ b/cura/PrinterOutput/ExtruderOuputModel.py @@ -36,28 +36,28 @@ class ExtruderOutputModel(QObject): self.activeMaterialChanged.emit() ## Update the hotend temperature. This only changes it locally. - def updateHotendTemperature(self, temperature: int): + def updateHotendTemperature(self, temperature: float): if self._hotend_temperature != temperature: self._hotend_temperature = temperature self.hotendTemperatureChanged.emit() - def updateTargetHotendTemperature(self, temperature: int): + def updateTargetHotendTemperature(self, temperature: float): if self._target_hotend_temperature != temperature: self._target_hotend_temperature = temperature self.targetHotendTemperatureChanged.emit() ## Set the target hotend temperature. This ensures that it's actually sent to the remote. - @pyqtSlot(int) - def setTargetHotendTemperature(self, temperature: int): + @pyqtSlot(float) + def setTargetHotendTemperature(self, temperature: float): self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self.updateTargetHotendTemperature(temperature) - @pyqtProperty(int, notify = targetHotendTemperatureChanged) - def targetHotendTemperature(self) -> int: + @pyqtProperty(float, notify = targetHotendTemperatureChanged) + def targetHotendTemperature(self) -> float: return self._target_hotend_temperature - @pyqtProperty(int, notify=hotendTemperatureChanged) - def hotendTemperature(self) -> int: + @pyqtProperty(float, notify=hotendTemperatureChanged) + def hotendTemperature(self) -> float: return self._hotend_temperature @pyqtProperty(str, notify = hotendIDChanged) From 80526893c277ce66853e0dae871ea3b0261a0f96 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 14 Dec 2017 09:36:31 +0100 Subject: [PATCH 080/135] Made setAcceptsCommands protected CL-541 --- cura/PrinterOutputDevice.py | 2 +- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index fdf9a77145..f8408947ab 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -138,7 +138,7 @@ class PrinterOutputDevice(QObject, OutputDevice): return self._accepts_commands ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands - def setAcceptsCommands(self, accepts_commands): + def _setAcceptsCommands(self, accepts_commands): if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 967c99995e..e8e340e333 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -79,9 +79,9 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. if self._authentication_state == AuthState.Authenticated: - self.setAcceptsCommands(True) + self._setAcceptsCommands(True) else: - self.setAcceptsCommands(False) + self._setAcceptsCommands(False) def _setupMessages(self): self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", From d3b9ac0d4550ca0dc24334d5be491b3563d963a5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 14 Dec 2017 17:37:57 +0100 Subject: [PATCH 081/135] Also start reworking the USBPrint. It's also time for some much needed code cleaning in that bit. The auto-detect is moved to it's own job, which should make it a whole lot easier to disable it all together. CL-541 --- cura/PrinterOutputDevice.py | 4 + plugins/USBPrinting/AutoDetectBaudJob.py | 44 + plugins/USBPrinting/USBPrinterOutputDevice.py | 792 ++---------------- .../USBPrinterOutputDeviceManager.py | 114 +-- 4 files changed, 146 insertions(+), 808 deletions(-) create mode 100644 plugins/USBPrinting/AutoDetectBaudJob.py diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index f8408947ab..458d0a1080 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -67,6 +67,10 @@ class PrinterOutputDevice(QObject, OutputDevice): self._connection_state = connection_state self.connectionStateChanged.emit(self._id) + @pyqtProperty(str, notify = connectionStateChanged) + def connectionState(self): + return self._connection_state + def _update(self): pass diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py new file mode 100644 index 0000000000..8dcc705397 --- /dev/null +++ b/plugins/USBPrinting/AutoDetectBaudJob.py @@ -0,0 +1,44 @@ +# 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 time import time +from serial import Serial, SerialException + +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 + + 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)) + 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 + + 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. \ No newline at end of file diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 1930f5402b..e406d0a479 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -1,752 +1,114 @@ # Copyright (c) 2016 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from .avr_isp import stk500v2, ispBase, intelHex -import serial # type: ignore -import threading -import time -import queue -import re -import functools - -from UM.Application import Application from UM.Logger import Logger -from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState -from UM.Message import Message -from UM.Qt.Duration import DurationFormat - -from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty - from UM.i18n import i18nCatalog +from UM.Application import Application + +from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState +from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + +from .AutoDetectBaudJob import AutoDetectBaudJob + +from serial import Serial, SerialException +from threading import Thread +from time import time + +import re + catalog = i18nCatalog("cura") class USBPrinterOutputDevice(PrinterOutputDevice): - def __init__(self, serial_port): + def __init__(self, serial_port, baud_rate = None): super().__init__(serial_port) self.setName(catalog.i18nc("@item:inmenu", "USB printing")) self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB")) self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB")) self.setIconName("print") - self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB")) self._serial = None self._serial_port = serial_port - self._error_state = None - self._connect_thread = threading.Thread(target = self._connect) - self._connect_thread.daemon = True + self._timeout = 3 - self._end_stop_thread = None - self._poll_endstop = False + self._use_auto_detect = True - # The baud checking is done by sending a number of m105 commands to the printer and waiting for a readable - # response. If the baudrate is correct, this should make sense, else we get giberish. - self._required_responses_auto_baud = 3 + self._baud_rate = baud_rate + self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] - self._listen_thread = threading.Thread(target=self._listen) - self._listen_thread.daemon = True + # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. + self._update_thread = Thread(target=self._update, daemon = True) - self._update_firmware_thread = threading.Thread(target= self._updateFirmware) - self._update_firmware_thread.daemon = True - self.firmwareUpdateComplete.connect(self._onFirmwareUpdateComplete) + self._last_temperature_request = None - self._heatup_wait_start_time = time.time() + def _autoDetectFinished(self, job): + result = job.getResult() + if result is not None: + self.setBaudRate(result) + self.connect() # Try to connect (actually create serial, etc) - self.jobStateChanged.connect(self._onJobStateChanged) - - ## Queue for commands that need to be send. Used when command is sent when a print is active. - self._command_queue = queue.Queue() - - self._is_printing = False - self._is_paused = False - - ## Set when print is started in order to check running time. - self._print_start_time = None - self._print_estimated_time = None - - ## Keep track where in the provided g-code the print is - self._gcode_position = 0 - - # List of gcode lines to be printed - self._gcode = [] - - # Check if endstops are ever pressed (used for first run) - self._x_min_endstop_pressed = False - self._y_min_endstop_pressed = False - self._z_min_endstop_pressed = False - - self._x_max_endstop_pressed = False - self._y_max_endstop_pressed = False - self._z_max_endstop_pressed = False - - # In order to keep the connection alive we request the temperature every so often from a different extruder. - # This index is the extruder we requested data from the last time. - self._temperature_requested_extruder_index = 0 - - self._current_z = 0 - - self._updating_firmware = False - - self._firmware_file_name = None - self._firmware_update_finished = False - - self._error_message = None - self._error_code = 0 - - onError = pyqtSignal() - - firmwareUpdateComplete = pyqtSignal() - firmwareUpdateChange = pyqtSignal() - - endstopStateChanged = pyqtSignal(str ,bool, arguments = ["key","state"]) - - def _setTargetBedTemperature(self, temperature): - Logger.log("d", "Setting bed temperature to %s", temperature) - self._sendCommand("M140 S%s" % temperature) - - def _setTargetHotendTemperature(self, index, temperature): - Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) - self._sendCommand("M104 T%s S%s" % (index, temperature)) - - def _setHeadPosition(self, x, y , z, speed): - self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) - - def _setHeadX(self, x, speed): - self._sendCommand("G0 X%s F%s" % (x, speed)) - - def _setHeadY(self, y, speed): - self._sendCommand("G0 Y%s F%s" % (y, speed)) - - def _setHeadZ(self, z, speed): - self._sendCommand("G0 Y%s F%s" % (z, speed)) - - def _homeHead(self): - self._sendCommand("G28 X") - self._sendCommand("G28 Y") - - def _homeBed(self): - self._sendCommand("G28 Z") - - ## Updates the target bed temperature from the printer, and emit a signal if it was changed. - # - # /param temperature The new target temperature of the bed. - # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature - def _updateTargetBedTemperature(self, temperature): - if self._target_bed_temperature == temperature: - return False - self._target_bed_temperature = temperature - self.targetBedTemperatureChanged.emit() - return True - - ## Updates the target hotend temperature from the printer, and emit a signal if it was changed. - # - # /param index The index of the hotend. - # /param temperature The new target temperature of the hotend. - # /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature - def _updateTargetHotendTemperature(self, index, temperature): - if self._target_hotend_temperatures[index] == temperature: - return False - self._target_hotend_temperatures[index] = temperature - self.targetHotendTemperaturesChanged.emit() - return True - - ## A name for the device. - @pyqtProperty(str, constant = True) - def name(self): - return self.getName() - - ## The address of the device. - @pyqtProperty(str, constant = True) - def address(self): - return self._serial_port - - def startPrint(self): - self.writeStarted.emit(self) - gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list") - self._updateJobState("printing") - self.printGCode(gcode_list) - - def _moveHead(self, x, y, z, speed): - self._sendCommand("G91") - self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) - self._sendCommand("G90") - - ## Start a print based on a g-code. - # \param gcode_list List with gcode (strings). - def printGCode(self, gcode_list): - Logger.log("d", "Started printing g-code") - if self._progress or self._connection_state != ConnectionState.connected: - self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer is busy or not connected."), title = catalog.i18nc("@info:title", "Printer Unavailable")) - self._error_message.show() - Logger.log("d", "Printer is busy or not connected, aborting print") - self.writeError.emit(self) - return - - self._gcode.clear() - for layer in gcode_list: - self._gcode.extend(layer.split("\n")) - - # Reset line number. If this is not done, first line is sometimes ignored - self._gcode.insert(0, "M110") - self._gcode_position = 0 - self._is_printing = True - self._print_start_time = time.time() - - for i in range(0, 4): # Push first 4 entries before accepting other inputs - self._sendNextGcodeLine() - - self.writeFinished.emit(self) - - ## Get the serial port string of this connection. - # \return serial port - def getSerialPort(self): - return self._serial_port - - ## Try to connect the serial. This simply starts the thread, which runs _connect. - def connect(self): - if not self._updating_firmware and not self._connect_thread.isAlive(): - self._connect_thread.start() - - ## Private function (threaded) that actually uploads the firmware. - def _updateFirmware(self): - Logger.log("d", "Attempting to update firmware") - self._error_code = 0 - self.setProgress(0, 100) - self._firmware_update_finished = False - - if self._connection_state != ConnectionState.closed: - self.close() - hex_file = intelHex.readHex(self._firmware_file_name) - - if len(hex_file) == 0: - Logger.log("e", "Unable to read provided hex file. Could not update firmware") - self._updateFirmwareFailedMissingFirmware() - return - - programmer = stk500v2.Stk500v2() - programmer.progress_callback = self.setProgress - - try: - programmer.connect(self._serial_port) - except Exception: - programmer.close() - pass - - # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases. - time.sleep(1) - - if not programmer.isConnected(): - Logger.log("e", "Unable to connect with serial. Could not update firmware") - self._updateFirmwareFailedCommunicationError() - return - - self._updating_firmware = True - - try: - programmer.programChip(hex_file) - self._updating_firmware = False - except serial.SerialException as e: - Logger.log("e", "SerialException while trying to update firmware: <%s>" %(repr(e))) - self._updateFirmwareFailedIOError() - return - except Exception as e: - Logger.log("e", "Exception while trying to update firmware: <%s>" %(repr(e))) - self._updateFirmwareFailedUnknown() - return - programmer.close() - - self._updateFirmwareCompletedSucessfully() - return - - ## Private function which makes sure that firmware update process has failed by missing firmware - def _updateFirmwareFailedMissingFirmware(self): - return self._updateFirmwareFailedCommon(4) - - ## Private function which makes sure that firmware update process has failed by an IO error - def _updateFirmwareFailedIOError(self): - return self._updateFirmwareFailedCommon(3) - - ## Private function which makes sure that firmware update process has failed by a communication problem - def _updateFirmwareFailedCommunicationError(self): - return self._updateFirmwareFailedCommon(2) - - ## Private function which makes sure that firmware update process has failed by an unknown error - def _updateFirmwareFailedUnknown(self): - return self._updateFirmwareFailedCommon(1) - - ## Private common function which makes sure that firmware update process has completed/ended with a set progress state - def _updateFirmwareFailedCommon(self, code): - if not code: - raise Exception("Error code not set!") - - self._error_code = code - - self._firmware_update_finished = True - self.resetFirmwareUpdate(update_has_finished = True) - self.progressChanged.emit() - self.firmwareUpdateComplete.emit() - - return - - ## Private function which makes sure that firmware update process has successfully completed - def _updateFirmwareCompletedSucessfully(self): - self.setProgress(100, 100) - self._firmware_update_finished = True - self.resetFirmwareUpdate(update_has_finished = True) - self.firmwareUpdateComplete.emit() - - return - - ## Upload new firmware to machine - # \param filename full path of firmware file to be uploaded - def updateFirmware(self, file_name): - Logger.log("i", "Updating firmware of %s using %s", self._serial_port, file_name) - self._firmware_file_name = file_name - self._update_firmware_thread.start() - - @property - def firmwareUpdateFinished(self): - return self._firmware_update_finished - - def resetFirmwareUpdate(self, update_has_finished = False): - self._firmware_update_finished = update_has_finished - self.firmwareUpdateChange.emit() - - @pyqtSlot() - def startPollEndstop(self): - if not self._poll_endstop: - self._poll_endstop = True - if self._end_stop_thread is None: - self._end_stop_thread = threading.Thread(target=self._pollEndStop) - self._end_stop_thread.daemon = True - self._end_stop_thread.start() - - @pyqtSlot() - def stopPollEndstop(self): - self._poll_endstop = False - self._end_stop_thread = None - - def _pollEndStop(self): - while self._connection_state == ConnectionState.connected and self._poll_endstop: - self.sendCommand("M119") - time.sleep(0.5) - - ## Private connect function run by thread. Can be started by calling connect. - def _connect(self): - Logger.log("d", "Attempting to connect to %s", self._serial_port) - self.setConnectionState(ConnectionState.connecting) - programmer = stk500v2.Stk500v2() - try: - programmer.connect(self._serial_port) # Connect with the serial, if this succeeds, it's an arduino based usb device. - self._serial = programmer.leaveISP() - except ispBase.IspError as e: - programmer.close() - Logger.log("i", "Could not establish connection on %s: %s. Device is not arduino based." %(self._serial_port,str(e))) - except Exception as e: - programmer.close() - Logger.log("i", "Could not establish connection on %s, unknown reasons. Device is not arduino based." % self._serial_port) - - # If the programmer connected, we know its an atmega based version. - # Not all that useful, but it does give some debugging information. - for baud_rate in self._getBaudrateList(): # Cycle all baud rates (auto detect) - Logger.log("d", "Attempting to connect to printer with serial %s on baud rate %s", self._serial_port, baud_rate) - if self._serial is None: - try: - self._serial = serial.Serial(str(self._serial_port), baud_rate, timeout = 3, writeTimeout = 10000) - time.sleep(10) - except serial.SerialException: - Logger.log("d", "Could not open port %s" % self._serial_port) - continue - else: - if not self.setBaudRate(baud_rate): - continue # Could not set the baud rate, go to the next - - time.sleep(1.5) # Ensure that we are not talking to the bootloader. 1.5 seconds seems to be the magic number - sucesfull_responses = 0 - timeout_time = time.time() + 5 - self._serial.write(b"\n") - self._sendCommand("M105") # Request temperature, as this should (if baudrate is correct) result in a command with "T:" in it - while timeout_time > time.time(): - line = self._readline() - if line is None: - Logger.log("d", "No response from serial connection received.") - # Something went wrong with reading, could be that close was called. - self.setConnectionState(ConnectionState.closed) - return - - if b"T:" in line: - Logger.log("d", "Correct response for auto-baudrate detection received.") - self._serial.timeout = 0.5 - sucesfull_responses += 1 - if sucesfull_responses >= self._required_responses_auto_baud: - self._serial.timeout = 2 # Reset serial timeout - self.setConnectionState(ConnectionState.connected) - self._listen_thread.start() # Start listening - Logger.log("i", "Established printer connection on port %s" % self._serial_port) - return - - self._sendCommand("M105") # Send M105 as long as we are listening, otherwise we end up in an undefined state - - Logger.log("e", "Baud rate detection for %s failed", self._serial_port) - self.close() # Unable to connect, wrap up. - self.setConnectionState(ConnectionState.closed) - - ## Set the baud rate of the serial. This can cause exceptions, but we simply want to ignore those. def setBaudRate(self, baud_rate): - try: - self._serial.baudrate = baud_rate - return True - except Exception as e: - return False + if baud_rate not in self._all_baud_rates: + Logger.log("w", "Not updating baudrate to {baud_rate} as it's an unknown baudrate".format(baud_rate=baud_rate)) + return - ## Close the printer connection - def close(self): - Logger.log("d", "Closing the USB printer connection.") - if self._connect_thread.isAlive(): + self._baud_rate = baud_rate + + def connect(self): + if self._baud_rate is None: + if self._use_auto_detect: + auto_detect_job = AutoDetectBaudJob(self._serial_port) + auto_detect_job.start() + auto_detect_job.finished.connect(self._autoDetectFinished) + return + if self._serial is None: try: - self._connect_thread.join() - except Exception as e: - Logger.log("d", "PrinterConnection.close: %s (expected)", e) - pass # This should work, but it does fail sometimes for some reason + self._serial = Serial(str(self._serial_port), self._baud_rate, timeout=self._timeout, writeTimeout=self._timeout) + except SerialException: + return + container_stack = Application.getInstance().getGlobalContainerStack() + num_extruders = container_stack.getProperty("machine_extruder_count", "value") - self._connect_thread = threading.Thread(target = self._connect) - self._connect_thread.daemon = True + # Ensure that a printer is created. + self._printers = [PrinterOutputModel(output_controller=None, number_of_extruders=num_extruders)] + self.setConnectionState(ConnectionState.connected) + self._update_thread.start() - self.setConnectionState(ConnectionState.closed) - if self._serial is not None: - try: - self._listen_thread.join() - except: - pass - if self._serial is not None: # Avoid a race condition when a thread can change the value of self._serial to None - self._serial.close() + def sendCommand(self, command): + if self._connection_state == ConnectionState.connected: + self._sendCommand(command) - self._listen_thread = threading.Thread(target = self._listen) - self._listen_thread.daemon = True - self._serial = None - - ## Directly send the command, withouth checking connection state (eg; printing). - # \param cmd string with g-code - def _sendCommand(self, cmd): + def _sendCommand(self, command): if self._serial is None: return - if "M109" in cmd or "M190" in cmd: - self._heatup_wait_start_time = time.time() + if type(command == str):q + command = (command + "\n").encode() + if not command.endswith(b"\n"): + command += b"\n" - try: - command = (cmd + "\n").encode() - self._serial.write(b"\n") - self._serial.write(command) - except serial.SerialTimeoutException: - Logger.log("w","Serial timeout while writing to serial port, trying again.") - try: - time.sleep(0.5) - self._serial.write((cmd + "\n").encode()) - except Exception as e: - Logger.log("e","Unexpected error while writing serial port %s " % e) - self._setErrorState("Unexpected error while writing serial port %s " % e) - self.close() - except Exception as e: - Logger.log("e","Unexpected error while writing serial port %s" % e) - self._setErrorState("Unexpected error while writing serial port %s " % e) - self.close() + self._serial.write(b"\n") + self._serial.write(command) - ## Send a command to printer. - # \param cmd string with g-code - def sendCommand(self, cmd): - if self._progress: - self._command_queue.put(cmd) - elif self._connection_state == ConnectionState.connected: - self._sendCommand(cmd) + def _update(self): + while self._connection_state == ConnectionState.connected and self._serial is not None: + line = self._serial.readline() + if self._last_temperature_request is None or time() > self._last_temperature_request + self._timeout: + # Timeout, or no request has been sent at all. + self.sendCommand("M105") + self._last_temperature_request = time() - ## Set the error state with a message. - # \param error String with the error message. - def _setErrorState(self, error): - self._updateJobState("error") - self._error_state = error - self.onError.emit() + if b"ok T:" in line or line.startswith(b"T:"): # Temperature message + extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) + # Update all temperature values + for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders): + extruder.updateHotendTemperature(float(match[1])) + extruder.updateTargetHotendTemperature(float(match[2])) - ## Request the current scene to be sent to a USB-connected printer. - # - # \param nodes A collection of scene nodes to send. This is ignored. - # \param file_name \type{string} A suggestion for a file name to write. - # \param filter_by_machine Whether to filter MIME types by machine. This - # is ignored. - # \param kwargs Keyword arguments. - def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): - container_stack = Application.getInstance().getGlobalContainerStack() - - if container_stack.getProperty("machine_gcode_flavor", "value") == "UltiGCode": - self._error_message = Message(catalog.i18nc("@info:status", "This printer does not support USB printing because it uses UltiGCode flavor."), title = catalog.i18nc("@info:title", "USB Printing")) - self._error_message.show() - return - elif not container_stack.getMetaDataEntry("supports_usb_connection"): - self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer does not support usb printing."), title = catalog.i18nc("@info:title", "Warning")) - self._error_message.show() - return - - self.setJobName(file_name) - self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds)) - - Application.getInstance().getController().setActiveStage("MonitorStage") - self.startPrint() - - def _setEndstopState(self, endstop_key, value): - if endstop_key == b"x_min": - if self._x_min_endstop_pressed != value: - self.endstopStateChanged.emit("x_min", value) - self._x_min_endstop_pressed = value - elif endstop_key == b"y_min": - if self._y_min_endstop_pressed != value: - self.endstopStateChanged.emit("y_min", value) - self._y_min_endstop_pressed = value - elif endstop_key == b"z_min": - if self._z_min_endstop_pressed != value: - self.endstopStateChanged.emit("z_min", value) - self._z_min_endstop_pressed = value - - ## Listen thread function. - def _listen(self): - Logger.log("i", "Printer connection listen thread started for %s" % self._serial_port) - container_stack = Application.getInstance().getGlobalContainerStack() - temperature_request_timeout = time.time() - ok_timeout = time.time() - while self._connection_state == ConnectionState.connected: - line = self._readline() - if line is None: - break # None is only returned when something went wrong. Stop listening - - if time.time() > temperature_request_timeout: - if self._num_extruders > 1: - self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders - self.sendCommand("M105 T%d" % (self._temperature_requested_extruder_index)) - else: - self.sendCommand("M105") - temperature_request_timeout = time.time() + 5 - - if line.startswith(b"Error:"): - # Oh YEAH, consistency. - # Marlin reports a MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n" - # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!" - # So we can have an extra newline in the most common case. Awesome work people. - if re.match(b"Error:[0-9]\n", line): - line = line.rstrip() + self._readline() - - # Skip the communication errors, as those get corrected. - if b"Extruder switched off" in line or b"Temperature heated bed switched off" in line or b"Something is wrong, please turn off the printer." in line: - if not self.hasError(): - self._setErrorState(line[6:]) - - elif b" T:" in line or line.startswith(b"T:"): # Temperature message - temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) - temperature_set = False - try: - for match in temperature_matches: - if match[0]: - extruder_nr = int(match[0]) - if extruder_nr >= container_stack.getProperty("machine_extruder_count", "value"): - continue - if match[1]: - self._setHotendTemperature(extruder_nr, float(match[1])) - temperature_set = True - if match[2]: - self._updateTargetHotendTemperature(extruder_nr, float(match[2])) - else: - requested_temperatures = match - if not temperature_set and requested_temperatures: - if requested_temperatures[1]: - self._setHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[1])) - if requested_temperatures[2]: - self._updateTargetHotendTemperature(self._temperature_requested_extruder_index, float(requested_temperatures[2])) - except: - Logger.log("w", "Could not parse hotend temperatures from response: %s", line) - # Check if there's also a bed temperature - temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line) - if container_stack.getProperty("machine_heated_bed", "value") and len(temperature_matches) > 0: - match = temperature_matches[0] - try: - if match[0]: - self._setBedTemperature(float(match[0])) - if match[1]: - self._updateTargetBedTemperature(float(match[1])) - except: - Logger.log("w", "Could not parse bed temperature from response: %s", line) - - elif b"_min" in line or b"_max" in line: - tag, value = line.split(b":", 1) - self._setEndstopState(tag,(b"H" in value or b"TRIGGERED" in value)) - - if self._is_printing: - if line == b"" and time.time() > ok_timeout: - line = b"ok" # Force a timeout (basically, send next command) - - if b"ok" in line: - ok_timeout = time.time() + 5 - if not self._command_queue.empty(): - self._sendCommand(self._command_queue.get()) - elif self._is_paused: - line = b"" # Force getting temperature as keep alive - else: - self._sendNextGcodeLine() - elif b"resend" in line.lower() or b"rs" in line: # Because a resend can be asked with "resend" and "rs" - try: - Logger.log("d", "Got a resend response") - self._gcode_position = int(line.replace(b"N:",b" ").replace(b"N",b" ").replace(b":",b" ").split()[-1]) - except: - if b"rs" in line: - self._gcode_position = int(line.split()[1]) - - # Request the temperature on comm timeout (every 2 seconds) when we are not printing.) - if line == b"": - if self._num_extruders > 1: - self._temperature_requested_extruder_index = (self._temperature_requested_extruder_index + 1) % self._num_extruders - self.sendCommand("M105 T%d" % self._temperature_requested_extruder_index) - else: - self.sendCommand("M105") - - Logger.log("i", "Printer connection listen thread stopped for %s" % self._serial_port) - - ## Send next Gcode in the gcode list - def _sendNextGcodeLine(self): - if self._gcode_position >= len(self._gcode): - return - line = self._gcode[self._gcode_position] - - if ";" in line: - line = line[:line.find(";")] - line = line.strip() - - # Don't send empty lines. But we do have to send something, so send - # m105 instead. - # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as - # an LCD menu pause. - if line == "" or line == "M0" or line == "M1": - line = "M105" - try: - if ("G0" in line or "G1" in line) and "Z" in line: - z = float(re.search("Z([0-9\.]*)", line).group(1)) - if self._current_z != z: - self._current_z = z - except Exception as e: - Logger.log("e", "Unexpected error with printer connection, could not parse current Z: %s: %s" % (e, line)) - self._setErrorState("Unexpected error: %s" %e) - checksum = functools.reduce(lambda x,y: x^y, map(ord, "N%d%s" % (self._gcode_position, line))) - - self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum)) - - progress = (self._gcode_position / len(self._gcode)) - - elapsed_time = int(time.time() - self._print_start_time) - self.setTimeElapsed(elapsed_time) - estimated_time = self._print_estimated_time - if progress > .1: - estimated_time = self._print_estimated_time * (1-progress) + elapsed_time - self.setTimeTotal(estimated_time) - - self._gcode_position += 1 - self.setProgress(progress * 100) - self.progressChanged.emit() - - ## Set the state of the print. - # Sent from the print monitor - def _setJobState(self, job_state): - if job_state == "pause": - self._is_paused = True - self._updateJobState("paused") - elif job_state == "print": - self._is_paused = False - self._updateJobState("printing") - elif job_state == "abort": - self.cancelPrint() - - def _onJobStateChanged(self): - # clear the job name & times when printing is done or aborted - if self._job_state == "ready": - self.setJobName("") - self.setTimeElapsed(0) - self.setTimeTotal(0) - - ## Set the progress of the print. - # It will be normalized (based on max_progress) to range 0 - 100 - def setProgress(self, progress, max_progress = 100): - self._progress = (progress / max_progress) * 100 # Convert to scale of 0-100 - if self._progress == 100: - # Printing is done, reset progress - self._gcode_position = 0 - self.setProgress(0) - self._is_printing = False - self._is_paused = False - self._updateJobState("ready") - self.progressChanged.emit() - - ## Cancel the current print. Printer connection wil continue to listen. - def cancelPrint(self): - self._gcode_position = 0 - self.setProgress(0) - self._gcode = [] - - # Turn off temperatures, fan and steppers - self._sendCommand("M140 S0") - self._sendCommand("M104 S0") - self._sendCommand("M107") - # Home XY to prevent nozzle resting on aborted print - # Don't home bed because it may crash the printhead into the print on printers that home on the bottom - self.homeHead() - self._sendCommand("M84") - self._is_printing = False - self._is_paused = False - self._updateJobState("ready") - Application.getInstance().getController().setActiveStage("PrepareStage") - - ## Check if the process did not encounter an error yet. - def hasError(self): - return self._error_state is not None - - ## private read line used by printer connection to listen for data on serial port. - def _readline(self): - if self._serial is None: - return None - try: - ret = self._serial.readline() - except Exception as e: - Logger.log("e", "Unexpected error while reading serial port. %s" % e) - self._setErrorState("Printer has been disconnected") - self.close() - return None - return ret - - ## Create a list of baud rates at which we can communicate. - # \return list of int - def _getBaudrateList(self): - ret = [115200, 250000, 230400, 57600, 38400, 19200, 9600] - return ret - - def _onFirmwareUpdateComplete(self): - self._update_firmware_thread.join() - self._update_firmware_thread = threading.Thread(target = self._updateFirmware) - self._update_firmware_thread.daemon = True - - self.connect() - - ## Pre-heats the heated bed of the printer, if it has one. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. This is - # ignored because there is no g-code to set this. - @pyqtSlot(float, float) - def preheatBed(self, temperature, duration): - Logger.log("i", "Pre-heating the bed to %i degrees.", temperature) - self._setTargetBedTemperature(temperature) - self.preheatBedRemainingTimeChanged.emit() - - ## Cancels pre-heating the heated bed of the printer. - # - # If the bed is not pre-heated, nothing happens. - @pyqtSlot() - def cancelPreheatBed(self): - Logger.log("i", "Cancelling pre-heating of the bed.") - self._setTargetBedTemperature(0) - self.preheatBedRemainingTimeChanged.emit() + bed_temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line) + match = bed_temperature_matches[0] + if match[0]: + self._printers[0].updateBedTemperature(float(match[0])) + if match[1]: + self._printers[0].updateTargetBedTemperature(float(match[1])) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 62412bb521..439ca1feaf 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -29,6 +29,9 @@ i18n_catalog = i18nCatalog("cura") ## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer. @signalemitter class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): + addUSBOutputDeviceSignal = Signal() + progressChanged = pyqtSignal() + def __init__(self, parent = None): super().__init__(parent = parent) self._serial_port_list = [] @@ -43,12 +46,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): 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 @@ -63,15 +60,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): 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 - def start(self): self._check_updates = True self._update_thread.start() @@ -79,58 +67,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 @@ -205,47 +163,17 @@ 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 - ## Create a list of serial ports on the system. # \param only_list_usb If true, only usb ports are listed def getSerialPortList(self, only_list_usb = False): From d3d9a6e1bb0dfb4c83688801b900c5b7d346e0b6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 13:25:35 +0100 Subject: [PATCH 082/135] Starting a print with USB printer now works with reworked printeroutputmodel CL-541 --- plugins/USBPrinting/USBPrinterOutputDevice.py | 126 ++++++++++++++++-- 1 file changed, 116 insertions(+), 10 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index e406d0a479..ace43e41d7 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -4,17 +4,21 @@ from UM.Logger import Logger from UM.i18n import i18nCatalog from UM.Application import Application +from UM.Qt.Duration import DurationFormat from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .AutoDetectBaudJob import AutoDetectBaudJob from serial import Serial, SerialException from threading import Thread from time import time +from queue import Queue import re +import functools # Used for reduce catalog = i18nCatalog("cura") @@ -32,9 +36,15 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._timeout = 3 + # List of gcode lines to be printed + self._gcode = [] + self._gcode_position = 0 + self._use_auto_detect = True self._baud_rate = baud_rate + + self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. @@ -42,6 +52,48 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._last_temperature_request = None + self._is_printing = False # A print is being sent. + + ## Set when print is started in order to check running time. + self._print_start_time = None + self._print_estimated_time = None + + # Queue for commands that need to be send. Used when command is sent when a print is active. + self._command_queue = Queue() + + ## Request the current scene to be sent to a USB-connected printer. + # + # \param nodes A collection of scene nodes to send. This is ignored. + # \param file_name \type{string} A suggestion for a file name to write. + # \param filter_by_machine Whether to filter MIME types by machine. This + # is ignored. + # \param kwargs Keyword arguments. + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): + gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list") + self._printGCode(gcode_list) + + ## Start a print based on a g-code. + # \param gcode_list List with gcode (strings). + def _printGCode(self, gcode_list): + self._gcode.clear() + + for layer in gcode_list: + self._gcode.extend(layer.split("\n")) + + # Reset line number. If this is not done, first line is sometimes ignored + self._gcode.insert(0, "M110") + self._gcode_position = 0 + self._is_printing = True + self._print_start_time = time() + + self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds)) + + for i in range(0, 4): # Push first 4 entries before accepting other inputs + self._sendNextGcodeLine() + + self.writeFinished.emit(self) + + def _autoDetectFinished(self, job): result = job.getResult() if result is not None: @@ -76,18 +128,19 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._update_thread.start() def sendCommand(self, command): - if self._connection_state == ConnectionState.connected: + if self._is_printing: + self._command_queue.put(command) + elif self._connection_state == ConnectionState.connected: self._sendCommand(command) def _sendCommand(self, command): if self._serial is None: return - if type(command == str):q + if type(command == str): command = (command + "\n").encode() if not command.endswith(b"\n"): command += b"\n" - self._serial.write(b"\n") self._serial.write(command) @@ -103,12 +156,65 @@ class USBPrinterOutputDevice(PrinterOutputDevice): extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) # Update all temperature values for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders): - extruder.updateHotendTemperature(float(match[1])) - extruder.updateTargetHotendTemperature(float(match[2])) + if match[1]: + extruder.updateHotendTemperature(float(match[1])) + if match[2]: + extruder.updateTargetHotendTemperature(float(match[2])) bed_temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line) - match = bed_temperature_matches[0] - if match[0]: - self._printers[0].updateBedTemperature(float(match[0])) - if match[1]: - self._printers[0].updateTargetBedTemperature(float(match[1])) + if bed_temperature_matches: + match = bed_temperature_matches[0] + if match[0]: + self._printers[0].updateBedTemperature(float(match[0])) + if match[1]: + self._printers[0].updateTargetBedTemperature(float(match[1])) + + if self._is_printing: + if b"ok" in line: + if not self._command_queue.empty(): + self._sendCommand(self._command_queue.get()) + else: + self._sendNextGcodeLine() + elif b"resend" in line.lower() or b"rs" in line: + # A resend can be requested either by Resend, resend or rs. + try: + self._gcode_position = int(line.replace(b"N:", b" ").replace(b"N", b" ").replace(b":", b" ").split()[-1]) + except: + if b"rs" in line: + # In some cases of the RS command it needs to be handled differently. + self._gcode_position = int(line.split()[1]) + + def _sendNextGcodeLine(self): + if self._gcode_position >= len(self._gcode): + return + line = self._gcode[self._gcode_position] + + if ";" in line: + line = line[:line.find(";")] + + line = line.strip() + + # Don't send empty lines. But we do have to send something, so send M105 instead. + # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause. + if line == "" or line == "M0" or line == "M1": + line = "M105" + + checksum = functools.reduce(lambda x, y: x ^ y, map(ord, "N%d%s" % (self._gcode_position, line))) + + self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum)) + + progress = (self._gcode_position / len(self._gcode)) + + elapsed_time = int(time() - self._print_start_time) + print_job = self._printers[0].activePrintJob + if print_job is None: + print_job = PrintJobOutputModel(output_controller = None) + self._printers[0].updateActivePrintJob(print_job) + + print_job.updateTimeElapsed(elapsed_time) + estimated_time = self._print_estimated_time + if progress > .1: + estimated_time = self._print_estimated_time * (1 - progress) + elapsed_time + print_job.updateTimeTotal(estimated_time) + + self._gcode_position += 1 From aef54f99dbb74d75225e53af09f57839436d5fca Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 13:43:19 +0100 Subject: [PATCH 083/135] If a print is completed, it's now also updated in UI CL-541 --- plugins/USBPrinting/USBPrinterOutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index ace43e41d7..6c5d4ecb7f 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -186,6 +186,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def _sendNextGcodeLine(self): if self._gcode_position >= len(self._gcode): + self._printers[0].updateActivePrintJob(None) + self._is_printing = False return line = self._gcode[self._gcode_position] From e2845a224cb1a0300cc3504ec71c749930d24906 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 14:01:58 +0100 Subject: [PATCH 084/135] No longer start print if it's already started CL-541 --- plugins/USBPrinting/USBPrinterOutputDevice.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 6c5d4ecb7f..7fbbb12be3 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -69,6 +69,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # is ignored. # \param kwargs Keyword arguments. def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): + if self._is_printing: + return # Aleady printing + + Application.getInstance().showPrintMonitor.emit(True) + gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list") self._printGCode(gcode_list) @@ -93,7 +98,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self.writeFinished.emit(self) - def _autoDetectFinished(self, job): result = job.getResult() if result is not None: @@ -210,7 +214,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): elapsed_time = int(time() - self._print_start_time) print_job = self._printers[0].activePrintJob if print_job is None: - print_job = PrintJobOutputModel(output_controller = None) + print_job = PrintJobOutputModel(output_controller = None, name= Application.getInstance().getPrintInformation().jobName) self._printers[0].updateActivePrintJob(print_job) print_job.updateTimeElapsed(elapsed_time) From a6deddb6aec95a778d34055934ed163548659dd5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 14:30:01 +0100 Subject: [PATCH 085/135] Added controller for USB printer CL-541 --- .../USBPrinting/USBPrinterOutputController.py | 57 +++++++++++++++++++ plugins/USBPrinting/USBPrinterOutputDevice.py | 8 ++- 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 plugins/USBPrinting/USBPrinterOutputController.py diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py new file mode 100644 index 0000000000..c42348ee6e --- /dev/null +++ b/plugins/USBPrinting/USBPrinterOutputController.py @@ -0,0 +1,57 @@ +# 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 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) \ No newline at end of file diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 7fbbb12be3..d7fd2cea7c 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -11,6 +11,7 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .AutoDetectBaudJob import AutoDetectBaudJob +from .USBPrinterOutputController import USBPrinterOuptutController from serial import Serial, SerialException from threading import Thread @@ -44,7 +45,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._baud_rate = baud_rate - self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. @@ -58,6 +58,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._print_start_time = None self._print_estimated_time = None + self._accepts_commands = True + # Queue for commands that need to be send. Used when command is sent when a print is active. self._command_queue = Queue() @@ -127,7 +129,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): num_extruders = container_stack.getProperty("machine_extruder_count", "value") # Ensure that a printer is created. - self._printers = [PrinterOutputModel(output_controller=None, number_of_extruders=num_extruders)] + self._printers = [PrinterOutputModel(output_controller=USBPrinterOuptutController(self), number_of_extruders=num_extruders)] self.setConnectionState(ConnectionState.connected) self._update_thread.start() @@ -214,7 +216,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): elapsed_time = int(time() - self._print_start_time) print_job = self._printers[0].activePrintJob if print_job is None: - print_job = PrintJobOutputModel(output_controller = None, name= Application.getInstance().getPrintInformation().jobName) + print_job = PrintJobOutputModel(output_controller = USBPrinterOuptutController(self), name= Application.getInstance().getPrintInformation().jobName) self._printers[0].updateActivePrintJob(print_job) print_job.updateTimeElapsed(elapsed_time) From 6bdce54e1dc5d0a4a47538a9091835a31e7d651a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 14:47:06 +0100 Subject: [PATCH 086/135] Enable progress bar for USB printing CL-541 --- plugins/USBPrinting/USBPrinterOutputDevice.py | 1 + .../USBPrinting/USBPrinterOutputDeviceManager.py | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index d7fd2cea7c..40964bdd20 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -217,6 +217,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): print_job = self._printers[0].activePrintJob if print_job is None: print_job = PrintJobOutputModel(output_controller = USBPrinterOuptutController(self), name= Application.getInstance().getPrintInformation().jobName) + print_job.updateState("printing") self._printers[0].updateActivePrintJob(print_job) print_job.updateTimeElapsed(elapsed_time) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 439ca1feaf..0f98c11ddf 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -46,20 +46,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): 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. - @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 - def start(self): self._check_updates = True self._update_thread.start() From 0ac48817b26713ea9a15ee2dea619f1485d5cace Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 15:00:10 +0100 Subject: [PATCH 087/135] Added abort, start & pause USL-541 --- .../USBPrinting/USBPrinterOutputController.py | 11 ++++++++ plugins/USBPrinting/USBPrinterOutputDevice.py | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py index c42348ee6e..ba45e7b0ca 100644 --- a/plugins/USBPrinting/USBPrinterOutputController.py +++ b/plugins/USBPrinting/USBPrinterOutputController.py @@ -31,6 +31,17 @@ class USBPrinterOuptutController(PrinterOutputController): 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. diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 40964bdd20..f1d7b1fdf4 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -60,6 +60,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._accepts_commands = True + self._paused = False + # Queue for commands that need to be send. Used when command is sent when a print is active. self._command_queue = Queue() @@ -83,6 +85,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # \param gcode_list List with gcode (strings). def _printGCode(self, gcode_list): self._gcode.clear() + self._paused = False for layer in gcode_list: self._gcode.extend(layer.split("\n")) @@ -179,6 +182,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice): if b"ok" in line: if not self._command_queue.empty(): self._sendCommand(self._command_queue.get()) + elif self._paused: + pass # Nothing to do! else: self._sendNextGcodeLine() elif b"resend" in line.lower() or b"rs" in line: @@ -190,6 +195,29 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # In some cases of the RS command it needs to be handled differently. self._gcode_position = int(line.split()[1]) + def pausePrint(self): + self._paused = True + + def resumePrint(self): + self._paused = False + + def cancelPrint(self): + self._gcode_position = 0 + self._gcode.clear() + self._printers[0].updateActivePrintJob(None) + self._is_printing = False + self._is_paused = False + + # Turn off temperatures, fan and steppers + self._sendCommand("M140 S0") + self._sendCommand("M104 S0") + self._sendCommand("M107") + + # Home XY to prevent nozzle resting on aborted print + # Don't home bed because it may crash the printhead into the print on printers that home on the bottom + self.printers[0].homeHead() + self._sendCommand("M84") + def _sendNextGcodeLine(self): if self._gcode_position >= len(self._gcode): self._printers[0].updateActivePrintJob(None) From 35d3690b8978345f753b6493b4bee26b7db75dea Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 15:19:59 +0100 Subject: [PATCH 088/135] Disable UMO checkup action --- plugins/UltimakerMachineActions/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/UltimakerMachineActions/__init__.py b/plugins/UltimakerMachineActions/__init__.py index 864c501392..495f212736 100644 --- a/plugins/UltimakerMachineActions/__init__.py +++ b/plugins/UltimakerMachineActions/__init__.py @@ -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() ]} From bd4797404df6761c84bf1b8b7f160957f8199805 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 15:46:15 +0100 Subject: [PATCH 089/135] Changed showMonitorStage to setActiveStage CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 4 ++-- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 6 +++--- plugins/USBPrinting/USBPrinterOutputDevice.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index ba82da64c3..eb9f0469fa 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -77,7 +77,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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().showPrintMonitor.emit(True) + Application.getInstance().getController().setActiveStage("MonitorStage") self.writeStarted.emit(self) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", []) @@ -160,7 +160,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - Application.getInstance().showPrintMonitor.emit(False) + Application.getInstance().getController().setActiveStage("PrepareStage") @pyqtSlot() def openPrintJobControlPanel(self): diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index e8e340e333..1f58da7ca9 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -177,7 +177,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): return # Notify the UI that a switch to the print monitor should happen - Application.getInstance().showPrintMonitor.emit(True) + Application.getInstance().getController().setActiveStage("MonitorStage") self.writeStarted.emit(self) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", []) @@ -265,7 +265,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - Application.getInstance().showPrintMonitor.emit(False) + Application.getInstance().getController().setActiveStage("PrepareStage") def _onPostPrintJobFinished(self, reply): self._progress_message.hide() @@ -290,7 +290,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): if button == QMessageBox.Yes: self._startPrint() else: - Application.getInstance().showPrintMonitor.emit(False) + 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. diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index f1d7b1fdf4..8b4961c19c 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -76,7 +76,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): if self._is_printing: return # Aleady printing - Application.getInstance().showPrintMonitor.emit(True) + Application.getInstance().getController().setActiveStage("MonitorStage") gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list") self._printGCode(gcode_list) From 1ae881caee70f1f47a4712a8592783f9ab7afb3a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 15 Dec 2017 16:03:58 +0100 Subject: [PATCH 090/135] USB device is first added and then trying to connect. This is to ensure that USB printers work without autodetect --- plugins/USBPrinting/USBPrinterOutputDevice.py | 2 +- plugins/USBPrinting/USBPrinterOutputDeviceManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 8b4961c19c..867233561e 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -127,10 +127,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice): try: self._serial = Serial(str(self._serial_port), self._baud_rate, timeout=self._timeout, writeTimeout=self._timeout) except SerialException: + Logger.log("w", "An exception occured while trying to create serial connection") return container_stack = Application.getInstance().getGlobalContainerStack() num_extruders = container_stack.getProperty("machine_extruder_count", "value") - # Ensure that a printer is created. self._printers = [PrinterOutputModel(output_controller=USBPrinterOuptutController(self), number_of_extruders=num_extruders)] self.setConnectionState(ConnectionState.connected) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 0f98c11ddf..e13d8cef39 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -157,8 +157,8 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): def addOutputDevice(self, serial_port): device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port) device.connectionStateChanged.connect(self._onConnectionStateChanged) - device.connect() self._usb_output_devices[serial_port] = device + device.connect() ## Create a list of serial ports on the system. # \param only_list_usb If true, only usb ports are listed From a35f665201a79b8975635b1ff0d60473bf08b1a6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Dec 2017 10:45:38 +0100 Subject: [PATCH 091/135] Fixed crash if the firmware was in the list, but not found. CL-541 --- plugins/USBPrinting/USBPrinterOutputDeviceManager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index e13d8cef39..c97d8c0160 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -135,7 +135,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 "" From 32cbd27b708fe6311edc538a8af6f15c57294a50 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Dec 2017 15:59:21 +0100 Subject: [PATCH 092/135] Fixed the firmware update for USB print CL-541 --- plugins/USBPrinting/AutoDetectBaudJob.py | 31 ++++- plugins/USBPrinting/FirmwareUpdateWindow.qml | 54 +++----- plugins/USBPrinting/USBPrinterOutputDevice.py | 125 +++++++++++++++++- .../USBPrinterOutputDeviceManager.py | 2 - .../UpgradeFirmwareMachineAction.qml | 10 +- 5 files changed, 171 insertions(+), 51 deletions(-) diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py index 8dcc705397..574e241453 100644 --- a/plugins/USBPrinting/AutoDetectBaudJob.py +++ b/plugins/USBPrinting/AutoDetectBaudJob.py @@ -4,9 +4,12 @@ from UM.Job import Job from UM.Logger import Logger -from time import time +from .avr_isp.stk500v2 import Stk500v2 + +from time import time, sleep from serial import Serial, SerialException + class AutoDetectBaudJob(Job): def __init__(self, serial_port): super().__init__() @@ -17,14 +20,30 @@ class AutoDetectBaudJob(Job): 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)) - 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 + 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 bootloader. 1.5 seconds seems to be the magic number successful_responses = 0 serial.write(b"\n") # Ensure we clear out previous responses diff --git a/plugins/USBPrinting/FirmwareUpdateWindow.qml b/plugins/USBPrinting/FirmwareUpdateWindow.qml index 44218b61b1..bd0c85f49d 100644 --- a/plugins/USBPrinting/FirmwareUpdateWindow.qml +++ b/plugins/USBPrinting/FirmwareUpdateWindow.qml @@ -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,10 +59,10 @@ 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; diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 867233561e..100643b490 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -5,6 +5,7 @@ from UM.Logger import Logger from UM.i18n import i18nCatalog from UM.Application import Application from UM.Qt.Duration import DurationFormat +from UM.PluginRegistry import PluginRegistry from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel @@ -12,19 +13,27 @@ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .AutoDetectBaudJob import AutoDetectBaudJob from .USBPrinterOutputController import USBPrinterOuptutController +from .avr_isp import stk500v2, intelHex + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty from serial import Serial, SerialException from threading import Thread -from time import time +from time import time, sleep from queue import Queue +from enum import IntEnum import re import functools # Used for reduce +import os catalog = i18nCatalog("cura") class USBPrinterOutputDevice(PrinterOutputDevice): + firmwareProgressChanged = pyqtSignal() + firmwareUpdateStateChanged = pyqtSignal() + def __init__(self, serial_port, baud_rate = None): super().__init__(serial_port) self.setName(catalog.i18nc("@item:inmenu", "USB printing")) @@ -50,6 +59,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. self._update_thread = Thread(target=self._update, daemon = True) + self._update_firmware_thread = Thread(target=self._updateFirmware, daemon = True) + self._last_temperature_request = None self._is_printing = False # A print is being sent. @@ -62,6 +73,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._paused = False + self._firmware_view = None + self._firmware_location = None + self._firmware_progress = 0 + self._firmware_update_state = FirmwareUpdateState.idle + # Queue for commands that need to be send. Used when command is sent when a print is active. self._command_queue = Queue() @@ -81,6 +97,88 @@ class USBPrinterOutputDevice(PrinterOutputDevice): gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list") self._printGCode(gcode_list) + + ## Show firmware interface. + # This will create the view if its not already created. + def showFirmwareInterface(self): + 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 updateFirmware(self, file): + self._firmware_location = file + self.showFirmwareInterface() + self.setFirmwareUpdateState(FirmwareUpdateState.updating) + self._update_firmware_thread.start() + + def _updateFirmware(self): + # Ensure that other connections are closed. + if self._connection_state != ConnectionState.closed: + self.close() + + hex_file = intelHex.readHex(self._firmware_location) + if len(hex_file) == 0: + Logger.log("e", "Unable to read provided hex file. Could not update firmware") + self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error) + return + + programmer = stk500v2.Stk500v2() + programmer.progress_callback = self._onFirmwareProgress + + try: + programmer.connect(self._serial_port) + except: + programmer.close() + Logger.logException("e", "Failed to update firmware") + self.setFirmwareUpdateState(FirmwareUpdateState.communication_error) + return + + # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases. + sleep(1) + if not programmer.isConnected(): + Logger.log("e", "Unable to connect with serial. Could not update firmware") + self.setFirmwareUpdateState(FirmwareUpdateState.communication_error) + try: + programmer.programChip(hex_file) + except SerialException: + self.setFirmwareUpdateState(FirmwareUpdateState.io_error) + return + except: + self.setFirmwareUpdateState(FirmwareUpdateState.unknown_error) + return + + programmer.close() + + # Clean up for next attempt. + self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True) + self._firmware_location = "" + self._onFirmwareProgress(100) + self.setFirmwareUpdateState(FirmwareUpdateState.completed) + + # Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later. + Application.getInstance().callLater(self.connect) + + @pyqtProperty(float, notify = firmwareProgressChanged) + def firmwareProgress(self): + return self._firmware_progress + + @pyqtProperty(int, notify=firmwareUpdateStateChanged) + def firmwareUpdateState(self): + return self._firmware_update_state + + def setFirmwareUpdateState(self, state): + if self._firmware_update_state != state: + self._firmware_update_state = state + self.firmwareUpdateStateChanged.emit() + + # Callback function for firmware update progress. + def _onFirmwareProgress(self, progress, max_progress = 100): + self._firmware_progress = (progress / max_progress) * 100 # Convert to scale of 0-100 + self.firmwareProgressChanged.emit() + ## Start a print based on a g-code. # \param gcode_list List with gcode (strings). def _printGCode(self, gcode_list): @@ -136,6 +234,15 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self.setConnectionState(ConnectionState.connected) self._update_thread.start() + def close(self): + super().close() + if self._serial is not None: + self._serial.close() + + # Re-create the thread so it can be started again later. + self._update_thread = Thread(target=self._update, daemon=True) + self._serial = None + def sendCommand(self, command): if self._is_printing: self._command_queue.put(command) @@ -155,7 +262,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def _update(self): while self._connection_state == ConnectionState.connected and self._serial is not None: - line = self._serial.readline() + try: + line = self._serial.readline() + except: + continue + if self._last_temperature_request is None or time() > self._last_temperature_request + self._timeout: # Timeout, or no request has been sent at all. self.sendCommand("M105") @@ -255,3 +366,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice): print_job.updateTimeTotal(estimated_time) self._gcode_position += 1 + + +class FirmwareUpdateState(IntEnum): + idle = 0 + updating = 1 + completed = 2 + unknown_error = 3 + communication_error = 4 + io_error = 5 + firmware_not_found_error = 6 \ No newline at end of file diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index c97d8c0160..47e2776286 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -6,7 +6,6 @@ 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 @@ -41,7 +40,6 @@ 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. diff --git a/plugins/UltimakerMachineActions/UpgradeFirmwareMachineAction.qml b/plugins/UltimakerMachineActions/UpgradeFirmwareMachineAction.qml index 72a77e992d..f36788daa5 100644 --- a/plugins/UltimakerMachineActions/UpgradeFirmwareMachineAction.qml +++ b/plugins/UltimakerMachineActions/UpgradeFirmwareMachineAction.qml @@ -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) } } } \ No newline at end of file From b4c83814d9774436c371ce0ae0165267fb4975a4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Dec 2017 16:03:48 +0100 Subject: [PATCH 093/135] Clean up unused imports CL-541 --- .../USBPrinterOutputDeviceManager.py | 18 ++++++++---------- plugins/USBPrinting/__init__.py | 11 ++++++----- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 47e2776286..4de71e8b23 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -2,32 +2,29 @@ # 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.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. @signalemitter -class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): +class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): addUSBOutputDeviceSignal = Signal() progressChanged = pyqtSignal() @@ -42,7 +39,8 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension): self._check_updates = True 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. + # 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 diff --git a/plugins/USBPrinting/__init__.py b/plugins/USBPrinting/__init__.py index 1cc45c3c3b..7bf5853c10 100644 --- a/plugins/USBPrinting/__init__.py +++ b/plugins/USBPrinting/__init__.py @@ -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()} From 79add4ffd8dc2534bafb204ce0c4ca4157dfc59d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Dec 2017 16:15:48 +0100 Subject: [PATCH 094/135] Added typing CL-541 --- plugins/USBPrinting/AutoDetectBaudJob.py | 7 +++-- plugins/USBPrinting/FirmwareUpdateWindow.qml | 3 +-- plugins/USBPrinting/USBPrinterOutputDevice.py | 26 ++++++++++--------- .../USBPrinterOutputDeviceManager.py | 2 +- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py index 574e241453..72f4f20262 100644 --- a/plugins/USBPrinting/AutoDetectBaudJob.py +++ b/plugins/USBPrinting/AutoDetectBaudJob.py @@ -10,6 +10,9 @@ 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__() @@ -43,7 +46,7 @@ class AutoDetectBaudJob(Job): serial.baudrate = baud_rate except: continue - sleep(1.5) # Ensure that we are not talking to the bootloader. 1.5 seconds seems to be the magic number + 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 @@ -60,4 +63,4 @@ class AutoDetectBaudJob(Job): return serial.write(b"M105\n") - self.setResult(None) # Unable to detect the correct baudrate. \ No newline at end of file + self.setResult(None) # Unable to detect the correct baudrate. diff --git a/plugins/USBPrinting/FirmwareUpdateWindow.qml b/plugins/USBPrinting/FirmwareUpdateWindow.qml index bd0c85f49d..e0f9de314e 100644 --- a/plugins/USBPrinting/FirmwareUpdateWindow.qml +++ b/plugins/USBPrinting/FirmwareUpdateWindow.qml @@ -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 @@ -68,7 +68,6 @@ UM.Dialog left: parent.left; right: parent.right; } - } SystemPalette diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 100643b490..1e28e252d1 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -22,6 +22,7 @@ from threading import Thread from time import time, sleep from queue import Queue from enum import IntEnum +from typing import Union, Optional, List import re import functools # Used for reduce @@ -34,20 +35,20 @@ class USBPrinterOutputDevice(PrinterOutputDevice): firmwareProgressChanged = pyqtSignal() firmwareUpdateStateChanged = pyqtSignal() - def __init__(self, serial_port, baud_rate = None): + def __init__(self, serial_port: str, baud_rate: Optional[int] = None): super().__init__(serial_port) self.setName(catalog.i18nc("@item:inmenu", "USB printing")) self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB")) self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB")) self.setIconName("print") - self._serial = None + self._serial = None # type: Optional[Serial] self._serial_port = serial_port self._timeout = 3 # List of gcode lines to be printed - self._gcode = [] + self._gcode = [] # type: List[str] self._gcode_position = 0 self._use_auto_detect = True @@ -61,13 +62,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._update_firmware_thread = Thread(target=self._updateFirmware, daemon = True) - self._last_temperature_request = None + self._last_temperature_request = None # type: Optional[int] self._is_printing = False # A print is being sent. ## Set when print is started in order to check running time. - self._print_start_time = None - self._print_estimated_time = None + self._print_start_time = None # type: Optional[int] + self._print_estimated_time = None # type: Optional[int] self._accepts_commands = True @@ -90,7 +91,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # \param kwargs Keyword arguments. def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): if self._is_printing: - return # Aleady printing + return # Aleady printing Application.getInstance().getController().setActiveStage("MonitorStage") @@ -181,7 +182,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): ## Start a print based on a g-code. # \param gcode_list List with gcode (strings). - def _printGCode(self, gcode_list): + def _printGCode(self, gcode_list: List[str]): self._gcode.clear() self._paused = False @@ -201,13 +202,13 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self.writeFinished.emit(self) - def _autoDetectFinished(self, job): + def _autoDetectFinished(self, job: AutoDetectBaudJob): result = job.getResult() if result is not None: self.setBaudRate(result) self.connect() # Try to connect (actually create serial, etc) - def setBaudRate(self, baud_rate): + def setBaudRate(self, baud_rate: int): if baud_rate not in self._all_baud_rates: Logger.log("w", "Not updating baudrate to {baud_rate} as it's an unknown baudrate".format(baud_rate=baud_rate)) return @@ -243,13 +244,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._update_thread = Thread(target=self._update, daemon=True) self._serial = None - def sendCommand(self, command): + ## Send a command to printer. + def sendCommand(self, command: Union[str, bytes]): if self._is_printing: self._command_queue.put(command) elif self._connection_state == ConnectionState.connected: self._sendCommand(command) - def _sendCommand(self, command): + def _sendCommand(self, command: Union[str, bytes]): if self._serial is None: return diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 4de71e8b23..58b6106fb0 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -22,7 +22,7 @@ import serial.tools.list_ports 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): addUSBOutputDeviceSignal = Signal() From 89004b8df5dcf7da7250e4af29a938d9cae417c8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Dec 2017 17:19:04 +0100 Subject: [PATCH 095/135] Fixed some QML warnings CL-541 --- resources/qml/PrinterOutput/HeatedBedBox.qml | 15 ++++++++++++++- .../qml/PrinterOutput/OutputDeviceHeader.qml | 1 - 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index 65c2a161bd..bc89da2251 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -302,7 +302,20 @@ Item } } font: UM.Theme.getFont("action_button") - text: printerModel.isPreheating ? catalog.i18nc("@button Cancel pre-heating", "Cancel") : catalog.i18nc("@button", "Pre-heat") + text: + { + if(printerModel == null) + { + return "" + } + if(printerModel.isPreheating ) + { + return catalog.i18nc("@button Cancel pre-heating", "Cancel") + } else + { + return catalog.i18nc("@button", "Pre-heat") + } + } } } } diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml index 6553655da0..ca64c79f2b 100644 --- a/resources/qml/PrinterOutput/OutputDeviceHeader.qml +++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml @@ -48,7 +48,6 @@ Item anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.right: parent.right anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.top: outputDevice.bottom } } } \ No newline at end of file From 37461a7934fb71d6cacf9bdab776e4f0952805db Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 19 Dec 2017 17:24:30 +0100 Subject: [PATCH 096/135] Made sendMaterialProfiles protected CL-541 --- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 1f58da7ca9..d0b8f139f1 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -138,7 +138,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_timer.stop() ## Send all material profiles to the printer. - def sendMaterialProfiles(self): + def _sendMaterialProfiles(self): Logger.log("i", "Sending material profiles to printer") # TODO: Might want to move this to a job... @@ -410,7 +410,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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() + self._sendMaterialProfiles() def _checkAuthentication(self): Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) From 95b1e8f68cb10071e6190fe6be7c890fd5b1675d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 20 Dec 2017 15:38:47 +0100 Subject: [PATCH 097/135] Ensured that multiple requests from the same camera are no longer possible CL-541 --- cura/PrinterOutput/NetworkCamera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py index b81914ca7d..f71a575c5f 100644 --- a/cura/PrinterOutput/NetworkCamera.py +++ b/cura/PrinterOutput/NetworkCamera.py @@ -45,6 +45,8 @@ class NetworkCamera(QObject): @pyqtSlot() def start(self): + # Ensure that previous requests (if any) are stopped. + self.stop() if self._target is None: Logger.log("w", "Unable to start camera stream without target!") return From 23330cd08601d38647e0cc8413cb51efa24e25ad Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 20 Dec 2017 15:39:52 +0100 Subject: [PATCH 098/135] Camera feed is also stopped when NetworkCamera is destroyed CL-541 --- cura/PrinterOutput/NetworkCamera.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py index f71a575c5f..ad4fb90dd2 100644 --- a/cura/PrinterOutput/NetworkCamera.py +++ b/cura/PrinterOutput/NetworkCamera.py @@ -87,6 +87,10 @@ class NetworkCamera(QObject): def getImage(self): return self._image + ## Ensure that close gets called when object is destroyed + def __del__(self): + self.close() + def _onStreamDownloadProgress(self, bytes_received, bytes_total): # An MJPG stream is (for our purpose) a stream of concatenated JPG images. # JPG images start with the marker 0xFFD8, and end with 0xFFD9 From d66e9493ca2a3c78bf05e00bb43066ed49c96069 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 20 Dec 2017 15:54:05 +0100 Subject: [PATCH 099/135] When not looking at camera, it will now actually be disabled CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index eb9f0469fa..ad085d16ec 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -133,6 +133,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): @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() From 041b1830fe5ab3d11303c2bbb264313fd5df50f2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 20 Dec 2017 16:30:40 +0100 Subject: [PATCH 100/135] Re-added manual printer adding CL-541 --- .../NetworkedPrinterOutputDevice.py | 3 + .../UM3NetworkPrinting/DiscoverUM3Action.py | 7 +- .../UM3NetworkPrinting/DiscoverUM3Action.qml | 4 +- .../UM3OutputDevicePlugin.py | 121 +++++++++++++++++- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 3585aee5ea..b10700176e 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -266,6 +266,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): else: return "" + def getProperties(self): + return self._properties + ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtProperty(str, constant=True) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index 84115f28d3..0e872fed43 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -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" @@ -30,8 +33,6 @@ class DiscoverUM3Action(MachineAction): # Time to wait after a zero-conf service change before allowing a zeroconf reset self._zero_conf_change_grace_period = 0.25 - discoveredDevicesChanged = pyqtSignal() - @pyqtSlot() def startDiscovery(self): if not self._network_plugin: @@ -73,7 +74,7 @@ class DiscoverUM3Action(MachineAction): self._network_plugin.removeManualDevice(key) if address != "": - self._network_plugin.addManualPrinter(address) + self._network_plugin.addManualDevice(address) def _onDeviceDiscoveryChanged(self, *args): self._last_zero_conf_event_time = time.time() diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index d79bd543e7..003fdbf95c 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -101,7 +101,7 @@ Cura.MachineAction id: removeButton text: catalog.i18nc("@action:button", "Remove") enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true" - onClicked: manager.removeManualPrinter(base.selectedDevice.key, base.selectedDevice.ipAddress) + onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress) } Button @@ -343,7 +343,7 @@ Cura.MachineAction onAccepted: { - manager.setManualPrinter(printerKey, addressText) + manager.setManualDevice(printerKey, addressText) } Column { diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 13ab774577..fa1a0bc417 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -5,14 +5,21 @@ 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 -from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice +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. @@ -35,6 +42,23 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): 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. @@ -62,6 +86,11 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): 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: @@ -94,6 +123,94 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): Logger.log("d", "zeroconf close...") self._zero_conf.close() + 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() + 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: From 4796e0057440f92639a2ac8e4e8d85001c0a9cc8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 20 Dec 2017 16:57:49 +0100 Subject: [PATCH 101/135] Fixed removing of manual printer CL-541 --- plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index fa1a0bc417..2e1fe4db6f 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -123,6 +123,16 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): 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) @@ -206,7 +216,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): if instance_name in self._discovered_devices: device = self._discovered_devices[instance_name] properties = device.getProperties().copy() - del properties[b"incomplete"] + 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) From e576c1a9f7936aad29d6a324c902c206e30043a8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 21 Dec 2017 10:01:29 +0100 Subject: [PATCH 102/135] Ensure that an update of icon also happens on output device change CL-541 --- plugins/MonitorStage/MonitorStage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index f223ef1844..41976c70a6 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -67,6 +67,8 @@ class MonitorStage(CuraStage): self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) self._setActivePrinter(self._printer_output_device.activePrinter) + # Force an update of the icon source + self._updateIconSource() except IndexError: pass From 9754aa5397b494b699b119b7e3fefe4f2f5a633a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 21 Dec 2017 13:16:44 +0100 Subject: [PATCH 103/135] Material & hotend updated callback is enabled for LegacyUM3 again CL-541 --- cura/PrinterOutputDevice.py | 15 ++- cura/Settings/MachineManager.py | 116 ++++++++++-------- .../LegacyUM3OutputDevice.py | 15 ++- .../UM3OutputDevicePlugin.py | 3 +- 4 files changed, 91 insertions(+), 58 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 458d0a1080..b4e67f6297 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -3,15 +3,14 @@ from UM.i18n import i18nCatalog from UM.OutputDevice.OutputDevice import OutputDevice -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl -from PyQt5.QtQml import QQmlComponent, QQmlContext +from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal +from PyQt5.QtWidgets import QMessageBox from UM.Logger import Logger from UM.Signal import signalemitter from UM.Application import Application -import os from enum import IntEnum # For the connection state tracking. from typing import List, Optional @@ -36,6 +35,12 @@ class PrinterOutputDevice(QObject, OutputDevice): connectionStateChanged = pyqtSignal(str) acceptsCommandsChanged = pyqtSignal() + # Signal to indicate that the material of the active printer on the remote changed. + materialIdChanged = pyqtSignal() + + # # Signal to indicate that the hotend of the active printer on the remote changed. + hotendIdChanged = pyqtSignal() + def __init__(self, device_id, parent = None): super().__init__(device_id = device_id, parent = parent) @@ -59,6 +64,10 @@ class PrinterOutputDevice(QObject, OutputDevice): self._connection_state = ConnectionState.closed + def materialHotendChangedMessage(self, callback): + Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'") + callback(QMessageBox.Yes) + def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index e78c0b9d97..50ab26f9df 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -117,7 +117,7 @@ class MachineManager(QObject): self._auto_hotends_changed = {} self._material_incompatible_message = Message(catalog.i18nc("@info:status", - "The selected material is incompatible with the selected machine or configuration."), + "The selected material is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Incompatible Material")) containers = ContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) @@ -135,21 +135,21 @@ class MachineManager(QObject): activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed - blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly + blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly outputDevicesChanged = pyqtSignal() def _onOutputDevicesChanged(self) -> None: - '''for printer_output_device in self._printer_output_devices: + for printer_output_device in self._printer_output_devices: printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged) - printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)''' + printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged) self._printer_output_devices = [] for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): if isinstance(printer_output_device, PrinterOutputDevice): self._printer_output_devices.append(printer_output_device) - #printer_output_device.hotendIdChanged.connect(self._onHotendIdChanged) - #printer_output_device.materialIdChanged.connect(self._onMaterialIdChanged) + printer_output_device.hotendIdChanged.connect(self._onHotendIdChanged) + printer_output_device.materialIdChanged.connect(self._onMaterialIdChanged) self.outputDevicesChanged.emit() @@ -169,58 +169,70 @@ class MachineManager(QObject): def totalNumberOfSettings(self) -> int: return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys()) - def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None: - if not self._global_container_stack: + def _onHotendIdChanged(self): + if not self._global_container_stack or not self._printer_output_devices: + return + + active_printer_model = self._printer_output_devices[0].activePrinter + if not active_printer_model: return - containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "variant", definition = self._global_container_stack.definition.getId(), name = hotend_id) - if containers: # New material ID is known - extruder_manager = ExtruderManager.getInstance() - machine_id = self.activeMachineId - extruders = extruder_manager.getMachineExtruders(machine_id) - matching_extruder = None - for extruder in extruders: - if str(index) == extruder.getMetaDataEntry("position"): - matching_extruder = extruder - break - if matching_extruder and matching_extruder.variant.getName() != hotend_id: - # Save the material that needs to be changed. Multiple changes will be handled by the callback. - self._auto_hotends_changed[str(index)] = containers[0]["id"] - self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) - else: - Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.definition.getId(), hotend_id)) + change_found = False + machine_id = self.activeMachineId + extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id), + key=lambda k: k.getMetaDataEntry("position")) - def _onMaterialIdChanged(self, index: Union[str, int], material_id: str): - if not self._global_container_stack: + for extruder_model, extruder in zip(active_printer_model.extruders, extruders): + containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="variant", + definition=self._global_container_stack.definition.getId(), + name=extruder_model.hotendID) + if containers: + # The hotend ID is known. + machine_id = self.activeMachineId + if extruder.variant.getName() != extruder_model.hotendID: + change_found = True + self._auto_hotends_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"] + + if change_found: + # A change was found, let the output device handle this. + self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) + + def _onMaterialIdChanged(self): + if not self._global_container_stack or not self._printer_output_devices: return - definition_id = "fdmprinter" - if self._global_container_stack.getMetaDataEntry("has_machine_materials", False): - definition_id = self.activeQualityDefinitionId - extruder_manager = ExtruderManager.getInstance() - containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "material", definition = definition_id, GUID = material_id) - if containers: # New material ID is known - extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId)) - matching_extruder = None - for extruder in extruders: - if str(index) == extruder.getMetaDataEntry("position"): - matching_extruder = extruder - break + active_printer_model = self._printer_output_devices[0].activePrinter + if not active_printer_model: + return - if matching_extruder and matching_extruder.material.getMetaDataEntry("GUID") != material_id: - # Save the material that needs to be changed. Multiple changes will be handled by the callback. - if self._global_container_stack.definition.getMetaDataEntry("has_variants") and matching_extruder.variant: - variant_id = self.getQualityVariantId(self._global_container_stack.definition, matching_extruder.variant) - for container in containers: - if container.get("variant") == variant_id: - self._auto_materials_changed[str(index)] = container["id"] - break - else: - # Just use the first result we found. - self._auto_materials_changed[str(index)] = containers[0]["id"] - self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) - else: - Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id)) + change_found = False + machine_id = self.activeMachineId + extruders = sorted(ExtruderManager.getInstance().getMachineExtruders(machine_id), + key=lambda k: k.getMetaDataEntry("position")) + + for extruder_model, extruder in zip(active_printer_model.extruders, extruders): + if extruder_model.activeMaterial is None: + continue + containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type="material", + definition=self._global_container_stack.definition.getId(), + GUID=extruder_model.activeMaterial.guid) + if containers: + # The material is known. + if extruder.material.getMetaDataEntry("GUID") != extruder_model.activeMaterial.guid: + change_found = True + if self._global_container_stack.definition.getMetaDataEntry("has_variants") and extruder.variant: + variant_id = self.getQualityVariantId(self._global_container_stack.definition, + extruder.variant) + for container in containers: + if container.get("variant") == variant_id: + self._auto_materials_changed[extruder.getMetaDataEntry("position")] = container["id"] + break + else: + # Just use the first result we found. + self._auto_materials_changed[extruder.getMetaDataEntry("position")] = containers[0]["id"] + if change_found: + # A change was found, let the output device handle this. + self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback) def _materialHotendChangedCallback(self, button): if button == QMessageBox.No: diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index d0b8f139f1..ce87eaba16 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -404,7 +404,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): Logger.log("d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", self._authentication_state) - print(reply.readAll()) self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() elif status_code == 200: @@ -533,6 +532,17 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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: @@ -547,6 +557,9 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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. diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 2e1fe4db6f..6bd1c24464 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -235,8 +235,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # 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)) - # TODO: For debug purposes; force it to be legacy printer. - #device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) + if cluster_size > 0: device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) else: From b1e9e3b8faecdf11a68754dd0126a6c53535c8c6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 21 Dec 2017 15:14:50 +0100 Subject: [PATCH 104/135] Prevent crash if disconnect already happend CL-541 --- plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 6bd1c24464..be62e68f03 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -227,7 +227,11 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): if device: if device.isConnected(): device.disconnect() - device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + try: + device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged) + except TypeError: + # Disconnect already happened. + pass self.discoveredDevicesChanged.emit() From 52d25042ebcf926e7af11a81e343c2572d3cd48a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 22 Dec 2017 11:29:13 +0100 Subject: [PATCH 105/135] Machines now re-appear after timeout CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index b10700176e..607d23aa53 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -51,6 +51,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._compressing_gcode = False self._gcode = [] + self._connection_state_before_timeout = None + def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): raise NotImplementedError("requestWrite needs to be implemented") @@ -114,7 +116,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if time_since_last_response > self._timeout_time >= time_since_last_request: # Go (or stay) into timeout. + if self._connection_state_before_timeout is None: + self._connection_state_before_timeout = self._connection_state + self.setConnectionState(ConnectionState.closed) + # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to # sleep. if time_since_last_response > self._recreate_network_manager_time: @@ -122,6 +128,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._createNetworkManager() if time() - self._last_manager_create_time > self._recreate_network_manager_time: self._createNetworkManager() + elif self._connection_state == ConnectionState.closed: + # Go out of timeout. + self.setConnectionState(self._connection_state_before_timeout) + self._connection_state_before_timeout = None return True From 931c87716bf9963bbc23398ae909b3608f05b2e7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 22 Dec 2017 11:31:48 +0100 Subject: [PATCH 106/135] Connection state changes now trigger a re-evaluation of the icon CL-541 --- plugins/MonitorStage/MonitorStage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 41976c70a6..b5a38dad70 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -59,12 +59,14 @@ class MonitorStage(CuraStage): 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 From eb27695d52181ef1abaadbb6fcbb9fa0772fd77b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 22 Dec 2017 12:04:02 +0100 Subject: [PATCH 107/135] If a reserved job is moved to a printer that can do it, it' s correclty removed from the old printer CL-541 --- cura/PrinterOutput/PrintJobOutputModel.py | 4 ++++ cura/PrinterOutput/PrinterOutputModel.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index fa8bbe8673..92376ad1dd 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -44,7 +44,11 @@ class PrintJobOutputModel(QObject): def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"): if self._assigned_printer != assigned_printer: + old_printer = self._assigned_printer self._assigned_printer = assigned_printer + if old_printer is not None: + # If the previously assigned printer is set, this job is moved away from it. + old_printer.updateActivePrintJob(None) self.assignedPrinterChanged.emit() @pyqtProperty(str, notify=keyChanged) diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 0c30d8d788..8234989519 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -180,11 +180,14 @@ class PrinterOutputModel(QObject): def updateActivePrintJob(self, print_job): if self._active_print_job != print_job: - if self._active_print_job is not None: - self._active_print_job.updateAssignedPrinter(None) + old_print_job = self._active_print_job + if print_job is not None: print_job.updateAssignedPrinter(self) self._active_print_job = print_job + + if old_print_job is not None: + old_print_job.updateAssignedPrinter(None) self.activePrintJobChanged.emit() def updateState(self, printer_state): From 562b2454b8bfeb8433fce2011058c04b8c137635 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 22 Dec 2017 13:48:51 +0100 Subject: [PATCH 108/135] Added missing notifications for Connect prints CL-541 --- .../ClusterUM3OutputDevice.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index ad085d16ec..d9740c4d29 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -67,6 +67,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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._finished_jobs = [] + @pyqtProperty(QObject, notify=activePrinterChanged) def controlItem(self): if self._active_printer is None: @@ -216,6 +218,24 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() + def _printJobStateChanged(self): + username = self._getUserName() + + if username is None: + # We only want to show notifications if username is set. + return + + 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() + + # 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 @@ -243,6 +263,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_job = PrintJobOutputModel(output_controller = ClusterUM3PrinterOutputController(self), key = print_job_data["uuid"], name = print_job_data["name"]) + print_job.stateChanged.connect(self._printJobStateChanged) job_list_changed = True self._print_jobs.append(print_job) print_job.updateTimeTotal(print_job_data["time_total"]) @@ -267,6 +288,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): for removed_job in removed_jobs: if removed_job.assignedPrinter: removed_job.assignedPrinter.updateActivePrintJob(None) + removed_job.stateChanged.disconnect(self._printJobStateChanged) self._print_jobs.remove(removed_job) job_list_changed = True From d6b0fcc92e44cd87756f8517a635f2e39751d8f9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 22 Dec 2017 13:50:55 +0100 Subject: [PATCH 109/135] Progress message is now also shown when still compressing g-code CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 1 + plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index d9740c4d29..9cf83e965a 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -106,6 +106,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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: diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index ce87eaba16..126dbbbde3 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -246,8 +246,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): 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. From 5e3666f073504e08da1c9ffbc7072831bd0452dc Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 2 Jan 2018 17:21:51 +0100 Subject: [PATCH 110/135] Fill out the type hints in NetworkedPrinterOutputDevice.py CL-541 --- .../NetworkedPrinterOutputDevice.py | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 607d23aa53..84e186dacb 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -9,7 +9,7 @@ from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtSignal, QUrl, QCoreApplication from time import time -from typing import Callable, Any, Optional +from typing import Callable, Any, Optional, Dict, Tuple from enum import IntEnum from typing import List @@ -27,45 +27,45 @@ class AuthState(IntEnum): class NetworkedPrinterOutputDevice(PrinterOutputDevice): authenticationStateChanged = pyqtSignal() - def __init__(self, device_id, address: str, properties, parent = None): + def __init__(self, device_id, address: str, properties, parent = None) -> None: super().__init__(device_id = device_id, parent = parent) - self._manager = None - self._last_manager_create_time = None + self._manager = None # type: QNetworkAccessManager + self._last_manager_create_time = None # type: float self._recreate_network_manager_time = 30 self._timeout_time = 10 # After how many seconds of no response should a timeout occur? - self._last_response_time = None - self._last_request_time = None + self._last_response_time = None # type: float + self._last_request_time = None # type: float self._api_prefix = "" self._address = address self._properties = properties self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()) - self._onFinishedCallbacks = {} + self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]] self._authentication_state = AuthState.NotAuthenticated - self._cached_multiparts = {} + self._cached_multiparts = {} # type: Dict[int, Tuple[QHttpMultiPart, QNetworkReply]] self._sending_gcode = False self._compressing_gcode = False - self._gcode = [] + self._gcode = [] # type: List[str] - self._connection_state_before_timeout = None + self._connection_state_before_timeout = None # type: Optional[ConnectionState] - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): + def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None: raise NotImplementedError("requestWrite needs to be implemented") - def setAuthenticationState(self, authentication_state): + def setAuthenticationState(self, authentication_state) -> None: if self._authentication_state != authentication_state: self._authentication_state = authentication_state self.authenticationStateChanged.emit() @pyqtProperty(int, notify=authenticationStateChanged) - def authenticationState(self): + def authenticationState(self) -> int: return self._authentication_state - def _compressDataAndNotifyQt(self, data_to_append): + def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes: 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. @@ -75,7 +75,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() return compressed_data - def _compressGCode(self): + def _compressGCode(self) -> Optional[bytes]: self._compressing_gcode = True ## Mash the data into single string @@ -87,7 +87,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if not self._compressing_gcode: self._progress_message.hide() # Stop trying to zip / send as abort was called. - return + return None 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. @@ -103,7 +103,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._compressing_gcode = False return byte_array_file_data - def _update(self): + def _update(self) -> bool: if self._last_response_time: time_since_last_response = time() - self._last_response_time else: @@ -135,7 +135,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): return True - def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json"): + def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json") -> QNetworkRequest: url = QUrl("http://" + self._address + self._api_prefix + target) request = QNetworkRequest(url) if content_type is not None: @@ -143,7 +143,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - def _createFormPart(self, content_header, data, content_type = None): + def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart: part = QHttpPart() if not content_header.startswith("form-data;"): @@ -158,18 +158,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): ## Convenience function to get the username from the OS. # The code was copied from the getpass module, as we try to use as little dependencies as possible. - def _getUserName(self): + def _getUserName(self) -> str: for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): user = os.environ.get(name) if user: return user return "Unknown User" # Couldn't find out username. - def _clearCachedMultiPart(self, reply): + def _clearCachedMultiPart(self, reply: QNetworkReply) -> None: if id(reply) in self._cached_multiparts: del self._cached_multiparts[id(reply)] - def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): + def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -178,7 +178,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): + def get(self, target: str, onFinished: Optional[Callable[[QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -187,13 +187,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def delete(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]): + def delete(self, target: str, onFinished: Optional[Callable[[QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() self._last_request_time = time() - pass - def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + def post(self, target: str, data: str, onFinished: Optional[Callable[[QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -204,7 +203,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target, content_type=None) @@ -223,17 +222,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None): + def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[QNetworkReply], None]], onProgress: Callable = None) -> None: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) post_part.setBody(body_data) self.postFormWithParts(target, [post_part], onFinished, onProgress) - def _onAuthenticationRequired(self, reply, authenticator): + def _onAuthenticationRequired(self, reply, authenticator) -> None: Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString())) - def _createNetworkManager(self): + def _createNetworkManager(self) -> None: Logger.log("d", "Creating network manager") if self._manager: self._manager.finished.disconnect(self.__handleOnFinished) @@ -246,7 +245,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._manager.authenticationRequired.connect(self._onAuthenticationRequired) #self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes - def __handleOnFinished(self, reply: QNetworkReply): + def __handleOnFinished(self, reply: QNetworkReply) -> None: # Due to garbage collection, we need to cache certain bits of post operations. # As we don't want to keep them around forever, delete them if we get a reply. if reply.operation() == QNetworkAccessManager.PostOperation: @@ -269,10 +268,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): Logger.logException("w", "something went wrong with callback") @pyqtSlot(str, result=str) - def getProperty(self, key): - key = key.encode("utf-8") - if key in self._properties: - return self._properties.get(key, b"").decode("utf-8") + def getProperty(self, key: str) -> str: + bytes_key = key.encode("utf-8") + if bytes_key in self._properties: + return self._properties.get(bytes_key, b"").decode("utf-8") else: return "" @@ -282,25 +281,25 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtProperty(str, constant=True) - def key(self): + def key(self) -> str: return self._id ## The IP address of the printer. @pyqtProperty(str, constant=True) - def address(self): + def address(self) -> str: return self._properties.get(b"address", b"").decode("utf-8") ## Name of the printer (as returned from the ZeroConf properties) @pyqtProperty(str, constant=True) - def name(self): + def name(self) -> str: return self._properties.get(b"name", b"").decode("utf-8") ## Firmware version (as returned from the ZeroConf properties) @pyqtProperty(str, constant=True) - def firmwareVersion(self): + def firmwareVersion(self) -> str: return self._properties.get(b"firmware_version", b"").decode("utf-8") ## IPadress of this printer @pyqtProperty(str, constant=True) - def ipAddress(self): - return self._address \ No newline at end of file + def ipAddress(self) -> str: + return self._address From ef46f514970edca956f9942cdf35a1a19ad9e4dd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 3 Jan 2018 13:44:38 +0100 Subject: [PATCH 111/135] Camera delete now triggers the correct function CL-541 --- cura/PrinterOutput/NetworkCamera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PrinterOutput/NetworkCamera.py b/cura/PrinterOutput/NetworkCamera.py index ad4fb90dd2..5b28ffd30d 100644 --- a/cura/PrinterOutput/NetworkCamera.py +++ b/cura/PrinterOutput/NetworkCamera.py @@ -89,7 +89,7 @@ class NetworkCamera(QObject): ## Ensure that close gets called when object is destroyed def __del__(self): - self.close() + self.stop() def _onStreamDownloadProgress(self, bytes_received, bytes_total): # An MJPG stream is (for our purpose) a stream of concatenated JPG images. From c1bf87bd8fac98150e720258087a3e1b90095f76 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 3 Jan 2018 13:54:15 +0100 Subject: [PATCH 112/135] Removed commented out code CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 84e186dacb..845696d80c 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -236,14 +236,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): Logger.log("d", "Creating network manager") if self._manager: self._manager.finished.disconnect(self.__handleOnFinished) - #self._manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged) self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) self._manager = QNetworkAccessManager() self._manager.finished.connect(self.__handleOnFinished) self._last_manager_create_time = time() self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - #self._manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged) # for debug purposes def __handleOnFinished(self, reply: QNetworkReply) -> None: # Due to garbage collection, we need to cache certain bits of post operations. From e12a2fbd6a8be70da77d09e59501cbb705e0ba85 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 3 Jan 2018 14:07:34 +0100 Subject: [PATCH 113/135] Fixed typing CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 845696d80c..46523e5989 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -178,7 +178,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def get(self, target: str, onFinished: Optional[Callable[[QNetworkReply], None]]) -> None: + def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -187,12 +187,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def delete(self, target: str, onFinished: Optional[Callable[[QNetworkReply], None]]) -> None: + def delete(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() self._last_request_time = time() - def post(self, target: str, data: str, onFinished: Optional[Callable[[QNetworkReply], None]], onProgress: Callable = None) -> None: + def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) @@ -203,7 +203,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[QNetworkReply], None]], onProgress: Callable = None) -> None: + def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target, content_type=None) @@ -222,7 +222,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[QNetworkReply], None]], onProgress: Callable = None) -> None: + def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) post_part.setBody(body_data) From 3173eb6740c9a7612dee1a28c1e885301b1a8688 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 3 Jan 2018 16:15:12 +0100 Subject: [PATCH 114/135] Avoid mega-tons of object copying when building compressed gcode CL-541 --- .../NetworkedPrinterOutputDevice.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 46523e5989..4914473ed1 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -80,28 +80,32 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): ## Mash the data into single string max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line. - byte_array_file_data = b"" - batched_line = "" + file_data_bytes_list = [] + batched_lines = [] + batched_lines_count = 0 for line in self._gcode: if not self._compressing_gcode: self._progress_message.hide() # Stop trying to zip / send as abort was called. return None - 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 += self._compressDataAndNotifyQt(batched_line) - batched_line = "" + batched_lines.append(line) + batched_lines_count += len(line) + + if batched_lines_count >= max_chars_per_line: + file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines))) + batched_lines = [] + batched_lines_count # Don't miss the last batch (If any) - if batched_line: - byte_array_file_data += self._compressDataAndNotifyQt(batched_line) + if len(batched_lines) != 0: + file_data_bytes_list.append(self._compressDataAndNotifyQt("".join(batched_lines))) self._compressing_gcode = False - return byte_array_file_data + return b"".join(file_data_bytes_list) def _update(self) -> bool: if self._last_response_time: From 487fca31dd06a63be85215abc8fa361db8b9b3e4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 3 Jan 2018 16:59:05 +0100 Subject: [PATCH 115/135] Chopped up bunch of functions. As per review request. CL-541 --- .../ClusterUM3OutputDevice.py | 279 ++++++++++-------- 1 file changed, 149 insertions(+), 130 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 9cf83e965a..c87fdaa0ba 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -216,15 +216,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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: - # We only want to show notifications if username is set. - return + 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"] @@ -244,144 +242,165 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply): - 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 print jobs message: Not valid JSON.") - return - print_jobs_seen = [] - job_list_changed = False - for print_job_data in result: - print_job = None - for job in self._print_jobs: - if job.key == print_job_data["uuid"]: - print_job = job - break + if not checkValidGetReply(reply): + return - if print_job is None: - print_job = PrintJobOutputModel(output_controller = ClusterUM3PrinterOutputController(self), - key = print_job_data["uuid"], - name = print_job_data["name"]) - print_job.stateChanged.connect(self._printJobStateChanged) - job_list_changed = True - self._print_jobs.append(print_job) - print_job.updateTimeTotal(print_job_data["time_total"]) - print_job.updateTimeElapsed(print_job_data["time_elapsed"]) - print_job.updateState(print_job_data["status"]) - print_job.updateOwner(print_job_data["owner"]) - printer = None - if print_job.state != "queued": - # Print job should be assigned to a printer. - printer = self._getPrinterByKey(print_job_data["printer_uuid"]) - else: # Status is queued - # The job can "reserve" a printer if some changes are required. - printer = self._getPrinterByKey(print_job_data["assigned_to"]) + result = loadJsonFromReply(reply) + if result is None: + return - if printer: - printer.updateActivePrintJob(print_job) + print_jobs_seen = [] + job_list_changed = False + for print_job_data in result: + print_job = findByKey(self._print_jobs, print_job_data["uuid"]) - print_jobs_seen.append(print_job) + if print_job is None: + print_job = self._createJobModel() + job_list_changed = True - # 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: - if removed_job.assignedPrinter: - removed_job.assignedPrinter.updateActivePrintJob(None) - removed_job.stateChanged.disconnect(self._printJobStateChanged) - self._print_jobs.remove(removed_job) - job_list_changed = True + self._updatePrintJob(print_job, print_job_data) - # Do a single emit for all print job changes. - if job_list_changed: - self.printJobsChanged.emit() + 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): - 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 printers state message: Not valid JSON.") - return - printer_list_changed = False - # TODO: Ensure that printers that have been removed are also removed locally. + if not checkValidGetReply(reply): + return - printers_seen = [] + result = loadJsonFromReply(reply) + if result is None: + return - for printer_data in result: - uuid = printer_data["uuid"] + printer_list_changed = False + printers_seen = [] - printer = None - for device in self._printers: - if device.key == uuid: - printer = device - break + for printer_data in result: + printer = findByKey(self._printers, printer_data["uuid"]) - if printer is None: - printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) - printer.setCamera(NetworkCamera("http://" + printer_data["ip_address"] + ":8080/?action=stream")) - self._printers.append(printer) - printer_list_changed = True - - printers_seen.append(printer) - - printer.updateName(printer_data["friendly_name"]) - printer.updateKey(uuid) - printer.updateType(printer_data["machine_variant"]) - if not printer_data["enabled"]: - printer.updateState("disabled") - else: - printer.updateState(printer_data["status"]) - - for index in range(0, self._number_of_extruders): - extruder = printer.extruders[index] - try: - extruder_data = printer_data["configuration"][index] - except IndexError: - break - - try: - hotend_id = extruder_data["print_core_id"] - except KeyError: - hotend_id = "" - extruder.updateHotendID(hotend_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"])) - # Unknown material. - 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) - removed_printers = [printer for printer in self._printers if printer not in printers_seen] - - for removed_printer in removed_printers: - self._printers.remove(removed_printer) + if printer is None: + printer = self._createPrinterModel(printer_data) printer_list_changed = True - if self._active_printer == removed_printer: - self._active_printer = None - self.activePrinterChanged.emit() - if printer_list_changed: - self.printersChanged.emit() + 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): + printer.updateName(data["friendly_name"]) + printer.updateKey(data["uuid"]) + printer.updateType(data["machine_variant"]) + if not data["enabled"]: + printer.updateState("disabled") else: - Logger.log("w", - "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) \ No newline at end of file + 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.assignedPrinter: + job.assignedPrinter.updateActivePrintJob(None) + job.stateChanged.disconnect(self._printJobStateChanged) + self._print_jobs.remove(job) + return True + return False + + 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 \ No newline at end of file From 42b50bf74974b07be9d50f99cd6eb09f5d5b03b5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 3 Jan 2018 17:14:52 +0100 Subject: [PATCH 116/135] Fixed typo in function CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c87fdaa0ba..52f39c42e9 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -255,7 +255,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_job = findByKey(self._print_jobs, print_job_data["uuid"]) if print_job is None: - print_job = self._createJobModel() + print_job = self._createPrintJobModel() job_list_changed = True self._updatePrintJob(print_job, print_job_data) From faa4af634afd47f04cafbdc97b23e8b57cbc9c3e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 09:58:15 +0100 Subject: [PATCH 117/135] Show pause text even if there is nothing to pause CL-541 --- resources/qml/MonitorButton.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index bd14cc58fe..0e9728da3d 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -277,10 +277,9 @@ Item (["paused", "printing"].indexOf(activePrintJob.state) >= 0) text: { - var result = ""; if (!printerConnected || activePrintJob == null) { - return ""; + return catalog.i18nc("@label:", "Pause"); } if (activePrintJob.state == "paused") From 9e055f03408b88005350ebaabb4153cb928d2d35 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 10:04:00 +0100 Subject: [PATCH 118/135] Added missing parameter CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 52f39c42e9..7aa6ebb03e 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -255,7 +255,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): print_job = findByKey(self._print_jobs, print_job_data["uuid"]) if print_job is None: - print_job = self._createPrintJobModel() + print_job = self._createPrintJobModel(print_job_data) job_list_changed = True self._updatePrintJob(print_job, print_job_data) From 6cf6d51feacb7030c86d0c09eee83e3026f09b2d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 10:06:09 +0100 Subject: [PATCH 119/135] Queued and printing amount now gets updated on state change CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 7aa6ebb03e..0e603d2816 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -232,6 +232,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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 From b6ebb804ba81f1e2920cefc7f95524958ed4b355 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 10:13:55 +0100 Subject: [PATCH 120/135] OutputDevice header now shows name of active printer CL-541 --- resources/qml/PrinterOutput/OutputDeviceHeader.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml index ca64c79f2b..d6ac863b87 100644 --- a/resources/qml/PrinterOutput/OutputDeviceHeader.qml +++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml @@ -13,11 +13,12 @@ Item implicitWidth: parent.width implicitHeight: Math.floor(childrenRect.height + UM.Theme.getSize("default_margin").height * 2) property var outputDevice: null + Rectangle { anchors.fill: parent color: UM.Theme.getColor("setting_category") - + property var activePrinter: outputDevice != null ? outputDevice.activePrinter : null Label { id: outputDeviceNameLabel @@ -26,7 +27,7 @@ Item anchors.left: parent.left anchors.top: parent.top anchors.margins: UM.Theme.getSize("default_margin").width - text: outputDevice != null ? outputDevice.name : catalog.i18nc("@info:status", "No printer connected") + text: outputDevice != null ? activePrinter.name : "" } Label { From 4cb7bc03ad7ccff89069dd2a3634c375fcb28620 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 10:30:03 +0100 Subject: [PATCH 121/135] Sidebar tooltips are now visible again CL-541 --- resources/qml/Cura.qml | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 0f55ad07fa..0aef23a2e1 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -383,6 +383,29 @@ UM.MainWindow anchors.top: parent.top } + Loader + { + id: main + + anchors + { + top: topbar.bottom + bottom: parent.bottom + left: parent.left + right: sidebar.left + } + + MouseArea + { + visible: UM.Controller.activeStage.mainComponent != "" + anchors.fill: parent + acceptedButtons: Qt.AllButtons + onWheel: wheel.accepted = true + } + + source: UM.Controller.activeStage.mainComponent + } + Loader { id: sidebar @@ -443,29 +466,6 @@ UM.MainWindow } } - Loader - { - id: main - - anchors - { - top: topbar.bottom - bottom: parent.bottom - left: parent.left - right: sidebar.left - } - - MouseArea - { - visible: UM.Controller.activeStage.mainComponent != "" - anchors.fill: parent - acceptedButtons: Qt.AllButtons - onWheel: wheel.accepted = true - } - - source: UM.Controller.activeStage.mainComponent - } - UM.MessageStack { anchors From 2588e11364976ee3133a0103f0496261b4462e45 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 4 Jan 2018 10:43:50 +0100 Subject: [PATCH 122/135] Improve and simplify how we hold on to form part objects CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 4914473ed1..73bd5e192b 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -45,7 +45,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]] self._authentication_state = AuthState.NotAuthenticated - self._cached_multiparts = {} # type: Dict[int, Tuple[QHttpMultiPart, QNetworkReply]] + # QHttpMultiPart objects need to be kept alive and not garbage collected during the + # HTTP which uses them. We hold references to these QHttpMultiPart objects here. + self._live_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart] self._sending_gcode = False self._compressing_gcode = False @@ -170,8 +172,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): return "Unknown User" # Couldn't find out username. def _clearCachedMultiPart(self, reply: QNetworkReply) -> None: - if id(reply) in self._cached_multiparts: - del self._cached_multiparts[id(reply)] + if reply in self._live_multiparts: + del self._live_multiparts[reply] def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if self._manager is None: @@ -219,7 +221,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.post(request, multi_post_part) - self._cached_multiparts[id(reply)] = (multi_post_part, reply) + self._live_multiparts[reply] = multi_post_part if onProgress is not None: reply.uploadProgress.connect(onProgress) From 2e8ae5c590de74fb47e0170436316bdfa966f814 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 12:54:47 +0100 Subject: [PATCH 123/135] Unsasigned jobs are now also removed from list CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 0e603d2816..244fb90b6b 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -275,6 +275,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # 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) @@ -371,12 +372,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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 - return False + self._print_jobs.remove(job) + + return True def _removePrinter(self, printer): self._printers.remove(printer) From 695c7d826706ebc59c707c6bb9eed4a5a25e348f Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 4 Jan 2018 13:26:13 +0100 Subject: [PATCH 124/135] Rename the `_live_multiparts` to the even better `_kept_alive_multiparts` name CL-541 --- cura/PrinterOutput/NetworkedPrinterOutputDevice.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 73bd5e192b..f2acc2115d 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -47,7 +47,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): # QHttpMultiPart objects need to be kept alive and not garbage collected during the # HTTP which uses them. We hold references to these QHttpMultiPart objects here. - self._live_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart] + self._kept_alive_multiparts = {} # type: Dict[QNetworkReply, QHttpMultiPart] self._sending_gcode = False self._compressing_gcode = False @@ -172,8 +172,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): return "Unknown User" # Couldn't find out username. def _clearCachedMultiPart(self, reply: QNetworkReply) -> None: - if reply in self._live_multiparts: - del self._live_multiparts[reply] + if reply in self._kept_alive_multiparts: + del self._kept_alive_multiparts[reply] def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if self._manager is None: @@ -221,7 +221,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.post(request, multi_post_part) - self._live_multiparts[reply] = multi_post_part + self._kept_alive_multiparts[reply] = multi_post_part if onProgress is not None: reply.uploadProgress.connect(onProgress) From acde48108dc83efa3adb8aba53f7b97fcbe54a86 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 13:31:54 +0100 Subject: [PATCH 125/135] Removed control item CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 244fb90b6b..51e713904b 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -69,14 +69,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._finished_jobs = [] - @pyqtProperty(QObject, notify=activePrinterChanged) - def controlItem(self): - if self._active_printer is None: - return super().controlItem - else: - # Let cura use the default. - return None - 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") From b20e35714fafe71b16664ec00ad12e8b1498fcf4 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 4 Jan 2018 13:33:53 +0100 Subject: [PATCH 126/135] Factor out the code for adding a function for run after a HTTP request CL-541 --- .../NetworkedPrinterOutputDevice.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index f2acc2115d..7cf855ee85 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -181,8 +181,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.put(request, data.encode()) - if onFinished is not None: - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + self._registerOnFinishedCallback(reply, onFinished) def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if self._manager is None: @@ -190,13 +189,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.get(request) - if onFinished is not None: - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished - - def delete(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: - if self._manager is None: - self._createNetworkManager() - self._last_request_time = time() + self._registerOnFinishedCallback(reply, onFinished) def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: @@ -206,8 +199,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.post(request, data) if onProgress is not None: reply.uploadProgress.connect(onProgress) - if onFinished is not None: - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + self._registerOnFinishedCallback(reply, onFinished) def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: @@ -225,8 +217,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if onProgress is not None: reply.uploadProgress.connect(onProgress) - if onFinished is not None: - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + self._registerOnFinishedCallback(reply, onFinished) def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: post_part = QHttpPart() @@ -249,6 +240,10 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_manager_create_time = time() self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: + if onFinished is not None: + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + def __handleOnFinished(self, reply: QNetworkReply) -> None: # Due to garbage collection, we need to cache certain bits of post operations. # As we don't want to keep them around forever, delete them if we get a reply. From c9b8b0333c87ba5021e4d29a3cba120ea1f0ad1d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 14:56:42 +0100 Subject: [PATCH 127/135] Sending prints to a specific printer in the cluster is now possible again CL-541 --- .../ClusterUM3OutputDevice.py | 30 ++++++++++++--- plugins/UM3NetworkPrinting/PrintWindow.qml | 38 +++++++++++-------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 51e713904b..d13706cc4c 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -62,11 +62,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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 = [] def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): @@ -79,11 +83,21 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Unable to find g-code. Nothing to send return - # TODO; DEBUG - self.sendPrintJob() + 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() @pyqtSlot() - def sendPrintJob(self): + @pyqtSlot(str) + def sendPrintJob(self, target_printer = ""): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( @@ -108,9 +122,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): parts = [] # If a specific printer was selected, it should be printed with that machine. - require_printer_name = "" # Todo; actually needs to be set - if require_printer_name: - parts.append(self._createFormPart("name=require_printer_name", bytes(require_printer_name, "utf-8"), "text/plain")) + 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")) @@ -324,6 +338,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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"]) diff --git a/plugins/UM3NetworkPrinting/PrintWindow.qml b/plugins/UM3NetworkPrinting/PrintWindow.qml index 7afe174da2..13d087f930 100644 --- a/plugins/UM3NetworkPrinting/PrintWindow.qml +++ b/plugins/UM3NetworkPrinting/PrintWindow.qml @@ -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 } } From d0ec7d10ce7570b5d1fde84dc126ba8283df6e11 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 4 Jan 2018 15:35:30 +0100 Subject: [PATCH 128/135] Printer discovery shows if a cluster is actually set up as a host CL-541 --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 6 ++++++ plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index d13706cc4c..4283042bf2 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -73,6 +73,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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") @@ -95,6 +97,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): 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 = ""): diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index be62e68f03..c639c25007 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -240,7 +240,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # or "Legacy" UM3 device. cluster_size = int(properties.get(b"cluster_size", -1)) - if cluster_size > 0: + if cluster_size >= 0: device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) else: device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties) From 39921795cee3e79d74267cd2092ecea4c0fee97a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 5 Jan 2018 14:46:16 +0100 Subject: [PATCH 129/135] Properly align printer not connected label from top --- resources/qml/PrinterOutput/OutputDeviceHeader.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml index d6ac863b87..bc9a44e245 100644 --- a/resources/qml/PrinterOutput/OutputDeviceHeader.qml +++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml @@ -19,6 +19,7 @@ Item anchors.fill: parent color: UM.Theme.getColor("setting_category") property var activePrinter: outputDevice != null ? outputDevice.activePrinter : null + Label { id: outputDeviceNameLabel @@ -29,6 +30,7 @@ Item anchors.margins: UM.Theme.getSize("default_margin").width text: outputDevice != null ? activePrinter.name : "" } + Label { id: outputDeviceAddressLabel @@ -39,6 +41,7 @@ Item anchors.right: parent.right anchors.margins: UM.Theme.getSize("default_margin").width } + Label { text: outputDevice != null ? outputDevice.connectionText : catalog.i18nc("@info:status", "The printer is not connected.") @@ -49,6 +52,8 @@ Item anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.right: parent.right anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.top: parent.top + anchors.topMargin: UM.Theme.getSize("default_margin").height } } } \ No newline at end of file From 2ca06f383e9d65a2b396f395427756ecbf8fafef Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 8 Jan 2018 09:41:18 +0100 Subject: [PATCH 130/135] USB printers also get their name set CL-541 --- plugins/USBPrinting/USBPrinterOutputDevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 1e28e252d1..100438e948 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -232,6 +232,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): num_extruders = container_stack.getProperty("machine_extruder_count", "value") # Ensure that a printer is created. self._printers = [PrinterOutputModel(output_controller=USBPrinterOuptutController(self), number_of_extruders=num_extruders)] + self._printers[0].updateName(container_stack.getName()) self.setConnectionState(ConnectionState.connected) self._update_thread.start() From df1bf419d93a909fcceeb052ed2daaf668b3d728 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 8 Jan 2018 11:44:30 +0100 Subject: [PATCH 131/135] Don't show "active print" header if no printer is connected CL-541 --- resources/qml/PrintMonitor.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 6fc4d8847d..471729192e 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -106,6 +106,7 @@ Column { label: catalog.i18nc("@label", "Active print") width: base.width + visible: activePrinter != null } @@ -114,6 +115,7 @@ Column label: catalog.i18nc("@label", "Job Name") value: activePrintJob != null ? activePrintJob.name : "" width: base.width + visible: activePrinter != null } MonitorItem @@ -121,6 +123,7 @@ Column label: catalog.i18nc("@label", "Printing Time") value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : "" width:base.width + visible: activePrinter != null } MonitorItem From 99de75a3fdafd5b1d653b638364dba2d61041712 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 8 Jan 2018 13:44:20 +0100 Subject: [PATCH 132/135] Fixed connection label state CL-541 --- cura/PrinterOutputDevice.py | 6 ++++++ plugins/USBPrinting/USBPrinterOutputDevice.py | 1 + resources/qml/PrinterOutput/OutputDeviceHeader.qml | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index b4e67f6297..2aa6fb382e 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -64,6 +64,12 @@ class PrinterOutputDevice(QObject, OutputDevice): self._connection_state = ConnectionState.closed + self._address = "" + + @pyqtProperty(str, constant = True) + def address(self): + return self._address + def materialHotendChangedMessage(self, callback): Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'") callback(QMessageBox.Yes) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 100438e948..c43d9a826b 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -44,6 +44,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._serial = None # type: Optional[Serial] self._serial_port = serial_port + self._address = serial_port self._timeout = 3 diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml index bc9a44e245..d5ce32786c 100644 --- a/resources/qml/PrinterOutput/OutputDeviceHeader.qml +++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml @@ -44,7 +44,7 @@ Item Label { - text: outputDevice != null ? outputDevice.connectionText : catalog.i18nc("@info:status", "The printer is not connected.") + text: outputDevice != null ? "" : catalog.i18nc("@info:status", "The printer is not connected.") color: outputDevice != null && outputDevice.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text") font: UM.Theme.getFont("very_small") wrapMode: Text.WordWrap From dcf02137adc8476c0adf77795873abc6991e4c90 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 9 Jan 2018 14:53:16 +0100 Subject: [PATCH 133/135] Fixed connecting for clean machine CL-541 --- plugins/UM3NetworkPrinting/DiscoverUM3Action.qml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index 003fdbf95c..0e58d8e991 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -29,13 +29,7 @@ Cura.MachineAction function connectToPrinter() { - if(base.selectedDevice) - { - var deviceKey = base.selectedDevice.key - manager.setKey(deviceKey); - completed(); - } - if(base.selectedPrinter && base.completeProperties) + if(base.selectedDevice && base.completeProperties) { var printerKey = base.selectedDevice.key if(manager.getStoredKey() != printerKey) From 2ce73a18397b0d0440a85a10d017c5568953d1b3 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 10 Jan 2018 00:07:43 +0100 Subject: [PATCH 134/135] Fix updating icon in the top bar This makes sure events such as onAcceptsCommandsChanges get connected if an outputdevice has been added before the monitorstage is initialized. --- plugins/MonitorStage/MonitorStage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index b5a38dad70..1a999ca896 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -77,6 +77,7 @@ class MonitorStage(CuraStage): 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() From 665c0a81f40959e39530b895b892937b97023bd4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 10 Jan 2018 16:29:36 +0100 Subject: [PATCH 135/135] Removed wrong comment --- plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 126dbbbde3..786b97d034 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -115,7 +115,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._not_authenticated_message.hide() self._requestAuthentication() - pass # Cura Connect doesn't do any authorization def connect(self): super().connect()