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/200] 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/200] 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} (91%) rename cura/PrinterOutput/{PrintJobModel.py => PrintJobOutputModel.py} (75%) 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 91% 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 75% 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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/200] 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 2ac3ef78c32b6a367f0d341b6bffcb14e0000732 Mon Sep 17 00:00:00 2001 From: Tyler Gibson Date: Wed, 29 Nov 2017 01:09:42 -0800 Subject: [PATCH 110/200] Adding support for Malyan M200, Monoprice Select Mini V1 & V2, renaming Malyan M180 for consistency. --- .../{m180.def.json => malyan_m180.def.json} | 1 + resources/definitions/malyan_m200.def.json | 90 ++++++++++++++++++ .../monoprice_select_mini_v1.def.json | 18 ++++ .../monoprice_select_mini_v2.def.json | 22 +++++ resources/meshes/malyan_m200_platform.stl | Bin 0 -> 29184 bytes .../malyan_m200/malyan_m200_0.04375.inst.cfg | 22 +++++ .../malyan_m200/malyan_m200_0.0875.inst.cfg | 22 +++++ .../malyan_m200/malyan_m200_0.13125.inst.cfg | 22 +++++ .../malyan_m200/malyan_m200_0.175.inst.cfg | 23 +++++ .../malyan_m200/malyan_m200_0.21875.inst.cfg | 22 +++++ .../malyan_m200/malyan_m200_0.2625.inst.cfg | 22 +++++ .../malyan_m200/malyan_m200_0.30625.inst.cfg | 22 +++++ .../malyan_m200/malyan_m200_0.35.inst.cfg | 23 +++++ resources/variants/malyan_m200_0.15.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.25.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.30.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.35.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.40.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.50.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.60.inst.cfg | 18 ++++ resources/variants/malyan_m200_0.80.inst.cfg | 18 ++++ resources/variants/malyan_m200_1.00.inst.cfg | 18 ++++ 22 files changed, 471 insertions(+) rename resources/definitions/{m180.def.json => malyan_m180.def.json} (98%) create mode 100644 resources/definitions/malyan_m200.def.json create mode 100644 resources/definitions/monoprice_select_mini_v1.def.json create mode 100644 resources/definitions/monoprice_select_mini_v2.def.json create mode 100644 resources/meshes/malyan_m200_platform.stl create mode 100644 resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg create mode 100644 resources/variants/malyan_m200_0.15.inst.cfg create mode 100644 resources/variants/malyan_m200_0.25.inst.cfg create mode 100644 resources/variants/malyan_m200_0.30.inst.cfg create mode 100644 resources/variants/malyan_m200_0.35.inst.cfg create mode 100644 resources/variants/malyan_m200_0.40.inst.cfg create mode 100644 resources/variants/malyan_m200_0.50.inst.cfg create mode 100644 resources/variants/malyan_m200_0.60.inst.cfg create mode 100644 resources/variants/malyan_m200_0.80.inst.cfg create mode 100644 resources/variants/malyan_m200_1.00.inst.cfg diff --git a/resources/definitions/m180.def.json b/resources/definitions/malyan_m180.def.json similarity index 98% rename from resources/definitions/m180.def.json rename to resources/definitions/malyan_m180.def.json index 71aa729b7e..5e0a6038dd 100644 --- a/resources/definitions/m180.def.json +++ b/resources/definitions/malyan_m180.def.json @@ -1,4 +1,5 @@ { + "id": "malyan_m180", "version": 2, "name": "Malyan M180", "inherits": "fdmprinter", diff --git a/resources/definitions/malyan_m200.def.json b/resources/definitions/malyan_m200.def.json new file mode 100644 index 0000000000..f19980a3b7 --- /dev/null +++ b/resources/definitions/malyan_m200.def.json @@ -0,0 +1,90 @@ +{ + "id": "malyan_m200", + "version": 2, + "name": "Malyan M200", + "inherits": "fdmprinter", + "metadata": { + "author": "Brian Corbino", + "manufacturer": "Malyan", + "category": "Other", + "file_formats": "text/x-gcode", + "platform": "malyan_m200_platform.stl", + "has_variants": true, + "has_variant_materials": false, + "has_materials": true, + "has_machine_materials": false, + "has_machine_quality": true, + "preferred_variant": "*0.4*", + "preferred_quality": "*0.175*", + "variants_name": "Nozzle size", + "supports_usb_connection": true, + "visible": true, + "first_start_actions": ["MachineSettingsAction"], + "supported_actions": ["MachineSettingsAction"] + }, + + "overrides": { + "machine_name": { "default_value": "Malyan M200" }, + "speed_print": { "default_value": 50 }, + "speed_wall_0": { "value": "round(speed_print * 0.75, 2)" }, + "speed_wall_x": { "value": "speed_print" }, + "speed_support": { "value": "speed_wall_0" }, + "speed_layer_0": { "value": "round(speed_print / 2.0, 2)" }, + "speed_travel": { "default_value": 50 }, + "speed_travel_layer_0": { "default_value": 40 }, + "speed_infill": { "value": "speed_print" }, + "speed_topbottom": {"value": "speed_print / 2"}, + + "layer_height": { "minimum_value": "0.04375", "maximum_value": "machine_nozzle_size * 0.875", "maximum_value_warning": "machine_nozzle_size * 0.48125 + 0.0875", "default_value": 0.13125 }, + "line_width": { "value": "round(machine_nozzle_size * 0.875, 2)" }, + + "material_print_temperature": { "minimum_value": "0" }, + "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, + "material_bed_temperature": { "minimum_value": "0" }, + "material_bed_temperature_layer_0": { "value": "material_bed_temperature + 5" }, + "material_standby_temperature": { "minimum_value": "0" }, + "machine_show_variants": { "default_value": true }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode" : { + "default_value": "G21;(metric values)\nG90;(absolute positioning)\nM82;(set extruder to absolute mode)\nM107;(start with the fan off)\nG28;(Home the printer)\nG92 E0;(Reset the extruder to 0)\nG0 Z5 E5 F500;(Move up and prime the nozzle)\nG0 X-1 Z0;(Move outside the printable area)\nG1 Y60 E8 F500;(Draw a priming/wiping line to the rear)\nG1 X-1;(Move a little closer to the print area)\nG1 Y10 E16 F500;(draw more priming/wiping)\nG1 E15 F250;(Small retract)\nG92 E0;(Zero the extruder)" + }, + "machine_end_gcode" : { + "default_value": "G0 X0 Y127;(Stick out the part)\nM190 S0;(Turn off heat bed, don't wait.)\nG92 E10;(Set extruder to 10)\nG1 E7 F200;(retract 3mm)\nM104 S0;(Turn off nozzle, don't wait)\nG4 S300;(Delay 5 minutes)\nM107;(Turn off part fan)\nM84;(Turn off stepper motors.)" + }, + "machine_width": { "default_value": 120 }, + "machine_depth": { "default_value": 120 }, + "machine_height": { "default_value": 120 }, + "machine_heated_bed": { "default_value": true }, + "machine_center_is_zero": { "default_value": false }, + "material_diameter": { "value": 1.75 }, + "machine_nozzle_size": { + "default_value": 0.4, + "minimum_value": "0.15" + }, + "machine_max_feedrate_x": { "default_value": 150 }, + "machine_max_feedrate_y": { "default_value": 150 }, + "machine_max_feedrate_z": { "default_value": 1.5 }, + "machine_max_feedrate_e": { "default_value": 100 }, + "machine_max_acceleration_x": { "default_value": 800 }, + "machine_max_acceleration_y": { "default_value": 800 }, + "machine_max_acceleration_z": { "default_value": 20 }, + "machine_max_acceleration_e": { "default_value": 10000 }, + "machine_max_jerk_xy": { "default_value": 20 }, + "machine_max_jerk_z": { "default_value": 0.4 }, + "machine_max_jerk_e": { "default_value": 5}, + "adhesion_type": { "default_value": "raft" }, + "raft_margin": { "default_value": 5 }, + "raft_airgap": { "default_value": 0.2625 }, + "raft_base_thickness": { "value": "0.30625" }, + "raft_interface_thickness": { "value": "0.21875" }, + "raft_surface_layers": { "default_value": 1 }, + "skirt_line_count": { "default_value": 2}, + "brim_width" : { "default_value": 5}, + "start_layers_at_same_position": { "default_value": true}, + "retraction_combing": { "default_value": "noskin" }, + "retraction_amount" : { "default_value": 4.5}, + "retraction_speed" : { "default_value": 40}, + "coasting_enable": { "default_value": true }, + "prime_tower_enable": { "default_value": false} + } +} diff --git a/resources/definitions/monoprice_select_mini_v1.def.json b/resources/definitions/monoprice_select_mini_v1.def.json new file mode 100644 index 0000000000..0b76a26c1c --- /dev/null +++ b/resources/definitions/monoprice_select_mini_v1.def.json @@ -0,0 +1,18 @@ +{ + "id": "monoprice_select_mini_v1", + "version": 2, + "name": "Monoprice Select Mini V1", + "inherits": "malyan_m200", + "metadata": { + "author": "Brian Corbino", + "manufacturer": "Monoprice", + "category": "Other", + "file_formats": "text/x-gcode", + "quality_definition": "malyan_m200", + "visible": true + }, + + "overrides": { + "machine_name": { "default_value": "Monoprice Select Mini V1" } + } +} diff --git a/resources/definitions/monoprice_select_mini_v2.def.json b/resources/definitions/monoprice_select_mini_v2.def.json new file mode 100644 index 0000000000..33367aad88 --- /dev/null +++ b/resources/definitions/monoprice_select_mini_v2.def.json @@ -0,0 +1,22 @@ +{ + "id": "monoprice_select_mini_v2", + "version": 2, + "name": "Monoprice Select Mini V2 (E3D)", + "inherits": "malyan_m200", + "metadata": { + "author": "Brian Corbino", + "manufacturer": "Monoprice", + "category": "Other", + "file_formats": "text/x-gcode", + "quality_definition": "malyan_m200", + "visible": true + }, + + "overrides": { + "machine_name": { "default_value": "Monoprice Select Mini V2" }, + "adhesion_type": { "default_value": "brim" }, + "retraction_combing": { "default_value": "noskin" }, + "retraction_amount" : { "default_value": 2.5}, + "retraction_speed" : { "default_value": 40} + } +} diff --git a/resources/meshes/malyan_m200_platform.stl b/resources/meshes/malyan_m200_platform.stl new file mode 100644 index 0000000000000000000000000000000000000000..32b19a09114b7b3dee2ccd2da1e7fac1cb1acaec GIT binary patch literal 29184 zcmb`PZLDV1RmUG%Gan2AGPxSGRV>L=X?@{CYn?{s@m}qVBMn$zVhD{|6DfA4Wnh?r z(p#oO2b@AHh)w*U^pXOl2->7^!Xz@!@DN|HiA`*+n$$E#8`4Awf?{k5b^rG||9yVz zoafw`58f}c=A89k|Mj-_J}>(`KYPXHSG@bO+5g+$+g~M9N9Hzo+wADZZL`mB%w`vs ze~)f#e&Dp?H$o`#M@1+O`z{X4J}$51P5jZ8<#I;#XxtfL-&Lt$Ov<_|*mI1vdJnflL|+9u?Anqe zA94`UbFfP?!Zom6MJ`##SI1#nMmW1VKKAM-{^j_SI@Pyyn^Cr_TGajn3}l>ym@-@%20Ry{F3m@r!p9jwuASB+J7<{K#(~s$KG< z&wTvUJOA~jW><2=5gcbm&@TD%_}H~$t8qv+2e~tXo=}VE&9(RcZp8717mx3`Uv%u}s~p*#LQqSxGVKbM&Z&P#7OVklbIaOGr(K&Iht4=!Y7w2ggjz$i zEZM8R+gBl^<>N;5alGn{?^xI?$@cw*_Z^#vg9vI#wxt{g?GkO-UiERTLP*QUjp*aJ z;Mbn`8M8}v96N8jw8^<1L{LkzEp4v7$WY z#BGkg9$&owmzr0ST@PE5hq)d^P|IE!(dUYGiMHHwRVygpRw1P2<3{vx?Ao!_;*jh( z_CNEnCg(Vapq6CUDR&9&5^dRD^^vbaNXy3?qSf5&I%8QGrH8potYyS7^5A&<4ezQP z=fC#*=EDvbLf!6aZjYdrYaR$*S$cHpjAg~a5uD?&mJyD_h%tv}GM%w34$gL&!&*j| z!-z2l*QMclCG^=Lq-9Hic;)W6X87AbKfY&pEfQWmSi1edlRd{2f?AT*yY{M$k2$Yc z%qOkViD!YJmgL?mGpP1lg}FLUdqo7b7?UdKzaw+5kX~!Y*|omBB7!w=HOIN)>czE> z-IKi{f?C7AvR_B)7<3jr9_0x~LFVAfLapKW=s7rBa;_bY+SLeZam;70N;lN`wd8{zvh}2hh^C%-{xB3Sc#yP z<(8|3b9L^~-)i-+EDrM3T&+Y<%W`nYzH7e!nct|j)fvm;AYZNdN(8km2Z!>c=*$0R z&%ONXr51P9dRVS;blj3R*QOBk%5vh^BMzO}d-*3Thh^C%U*i}dsAV}gx?I&!r3k;Z z=kCg3*>Q+F;+R6vE6c&5S`<{}P^I?Xbx*|6$+AnnR`U^pT9$*O>%@epzJpJlc>J8- zuiUM7M+jx+aRS_CFj>EFF&JIB>%Pm)$ODczE0N2B^ILO!Lk`aPhmRrp=Cq^7) z?sH#hR&$-n*XG0#f?Ae?qjdXR`6~aL(XM4_?R@o3r4})6YVWQ@P|I@EL$>{7jZY)^ z#DmWUyhg>fJJ0wDl%7-4QburPp%&2@!85*2(7NPn39o8>97Is7JRNM$dNaGAr2x?gtcg=3P zBHMd;jPtm%c&NrizG(!tEC)x)?%*2kfm-vpLW=o=eC_(G&OX+vxvE!Z;gu1Iqg|1? zucYPr+fwAJ^J;{kmgSVI+KGzLom(81WtV)d<~4F_$riON2S?Yt6;Wp7Ru9YKu%(D&3PCN) z!J%3RRsN^z-PTu@WtV(y42}@gvK$E7JYQK$wy0$} zI5=M!QGExWI#Jg?+asuDS(a+I>x!(#p^@V_EKBR`5Y)2Va@FoRH3PUFmc>E7e0^mt z*`k)^;NbO@W;OSfWpUV2L_UR}mgV43o-_lM|LJ*2<*2@cFVECd2x?gluR26qKlI#C z?7^LJ=#@PTjxs*d`r&7mQMKEv@3s_i zOd+UcIXH%^0iIcwF>$PNkgs)njXZiZo^n#la^pzrhvSIx7;)HA#L~bdk@?|7v#e zjI6BfcSk+yI=JeYrE+3~e79GYgQKf?9}$a*v5I4rB%AUtSPJ3 z4n{D!^0^P+T6@fLApZWR))tSQyt3x% z&V9F(rv}=KxV3n5LA>Rjk5+VlT=(bS_2QxV+VjETGxFWC(B_E9iSQY*TOOh_hqZ(t zM=#5(H(ht};)SohZN9$z%*jt(a_#b>*Bz?9|H9YzFaPJ2o2&fX%h$ALq#3dI+z)oS z+FDSH=!Do!Eu!E0z5AE1x$5Sc>1hPD_PucJ^38vBs6%}F%8M8GycEw~X_ub##}zpd zwA*WKEi6Yp9^19H__@Ena{dEd|-L$+do=47&+0?2x`&WItEACmA~Y$9C4JnWbV0yXDq9i=xy?9H?_W4 zy2P^&J~-uc8EzZ!34o<6j%jGA~WV_Erm z;Q8z7`{4r}R*Wp#U=+e;jCN8Z2Znw{SDi9^fJzvqUU-Tt_;`>7WX z%rAP!XHOmb_Pr;m#q6FP;*&=YF8}k!{b#5k?K-21$;VqgMses2J-Os34=wNe&Fi~W z4RP$|86wOPE6x;xT7UV^2bWJD_&~>jH;O0uMv?C}$(P=Y?*5}YZ(ctA?7?b_?;SIu zjM~dQ&iga7)4ja4;5*hjGc*J_h_D>lEhB2aY78=RR6uF!EDK0}0LA0H!VmwPm(5!_!wEuwQA+^uR^-!5?n zZANept#|+WYp2yU*WIYjlyQ8z_oT}0c;@hk62kT9zC>%|F(a&{-84pP)cUx;^Xlgk zce`p(y1ni_<~Y~!%;8ZqM9@-3aD0%1mNLSlXvjfJ8Q~rsB4{ZiJPw8kTFMA_;t)Yg z8R705B4{Zi+-EKJ}G@gBiUsS2GIzIM_2+O*1 zB06(uJez}drxDaDS!f<=hO?#2u?o?3{sh9dq@{UKb|w4xxaQQdEUjq-y`mP;IS!t2 zcEv$U8KL=AzWWO&$Km6ODI;7DopF~)_VID#)UqtCX$1YH7SY)&BB;fl&Iot4eAn#k z$bEbq2em9qYZ^gzY7w2i;u(&4TFMBm6Y^bGFP0UPRs+YB5&ptS2(19t@^PUv!t9!B zci|h8vqMmeeA67%nvR2BQHxoa=Af3dzJ27>ay7R{P|IC8M!3Uu#(k_zPa`-wiJ+w% zxo3kR2Q6iU=aM0UmNLR~)DS^S8BuzvL>x0G`V$zA;nj6xa{{pl7k3p3B9@= zL{O`|eXV;{#h1O}89uY3rHtSiBBpCj1Up6uXCZUM+v107eu$n%L;!;uBh;rxDBvbF~ek+jT2X28f9YGQzMM`(&7r9LHEII)54wJ80TB z25n~GSjik5gLz)*M^{F~D2jLSLi^W&V)pU=D~^x6FQZ3rA4ukiQ5${P zHKH>@9p}9`k)QZQD8-?@IOFW9cfHev2wKV($iUE?e$hajeR8 z&J}CGOsiv7&kX7Y*P~qTmO8h&WCEeFX0M2#rR-Ii)moPjK}#9IIOsdk8NoB;EA#F| z?@G(H<^18DL*v=!q4N*5eKH(Hob9H)q9@ZF^om+UPje9Ax69)=m@8^Y_Ad&GBi_8K z>xq!eUq)2N6qRxxRrYwzy(`T7`owNoTK-aZdjz#CM;zQc=skBs4)X@=5e3^s( zQj6$}@O~e8#d{py`(#9!)!O|)nW@`q5b~<4CGMHvNaaqI%;7GPSF9SVnGv)_4(x

Kah*fmelOmn**YEV59%c@nVQpwBBRCG? z3dzsQ`pRYmefNr^Xvv)sJi|=WQbzC$GcEM$CoDuzE3RZ7u1{X3IjF@R%LrZLnw{0u zm8`zh9oL+BXFcLNcvUCTE;}Qx8U5vbS?9XH`mC>9C&n3iLQ6T0Vz=ClM#oW0Gn_4D z1kVr=*TJhIFEuDv#qmV2)TTWbEtxJ?&MOi1+m(oeT102BN@tY&j2MIB^D{&tJZ=(V zVXcz9)7FwKK7YHI5!6!N{3Y5nLb>v{mcADehewy>%t0-$QD;J=y*M?yLb$SH4xVu} zrKOcq&#cD?=E|9t7BiCL&>7pJ7SS2OGwv93*iuH6(OG*mw4#rr8S)o#Ok*%t5cHMRZ0CYTi7Y_R1@# z{AC;*!I?uf@K;-b2;UXhn`FnH%})7zE{j%$#;Lf7-K$u`Hkp|P~_%_{KVa_^5$>wGJ;w~F5bmg ztgmiOEj?+}8GhAP zYSZSij3}OzEB&Y~wS0#><|xrNTSQMIe7`JP{HA3Z!R(gWw8+c&Xc@_I5J9bSyp67m zke27fKu0f4BRInmt!nnamrOn1{M_#o#gPzdCj9@pgOH@tw@ic;3s^;q6 zRn2WHJc$|Z>=3LV(bHbh?leN(kr6R4nYrz&R&&SX6fr%+NFm=^uKPbc}WP}TT1qQjb&x}%m``O(rQHi zcA;O-&Efmn?3KqFwIus$AVogsQ!YLQ=CjO<;L}sjiSmTc7Bj*Xl$N_sGA$VqPte`f z(z5Ja4dWOi7_DVlTFoK5=8)E^E(s2w8EO%2j$*aE4d5!{$i?T3NX`g;A?@r+iiQ;X|kM$j%-OQJKvPtO%O`KCE^ zpAl~tW~cdGGV7E%sKp9SBls;c5ww&!7%dUBlo5X3uE=?ZG<{dZJFHmS<2O2q&K!QS zEe?Jy$uC?pf?E8Za~e^`bDODSjf!9OWLIVmYQ-3gIHnQ&3X`7D!^}ai#Npouj5RTy zISdi;3|YB~K8{~mW!8s?{=02@!k99GT8wrY5my%Z7h>fhosh3Uwd zA6+f^8PGIB9RB@8)?+#jo?+GE`XQc7<;dA3%=ENZw8gF@cji#!jzg=AuTkQf@!_(j z57&>lx{r>d7SYojtp7BE{!)wR%pom5Nh=<fbC45Z$wIWszD$Z?63x_DO+X literal 0 HcmV?d00001 diff --git a/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg new file mode 100644 index 0000000000..c19d6f2050 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg @@ -0,0 +1,22 @@ +[general] +version = 2 +name = M1 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = 2 +quality_type = fine +setting_version = 3 + +[values] +layer_height = 0.04375 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg new file mode 100644 index 0000000000..519e963227 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg @@ -0,0 +1,22 @@ +[general] +version = 2 +name = M2 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = 1 +quality_type = high +setting_version = 3 + +[values] +layer_height = 0.0875 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg new file mode 100644 index 0000000000..732218cd79 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg @@ -0,0 +1,22 @@ +[general] +version = 2 +name = M3 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = 0 +quality_type = normal +setting_version = 3 + +[values] +layer_height = 0.13125 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg new file mode 100644 index 0000000000..f27e1ff900 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = M4 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = -1 +quality_type = fast +global_quality = true +setting_version = 3 + +[values] +layer_height = 0.175 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg new file mode 100644 index 0000000000..3ff3b562b2 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg @@ -0,0 +1,22 @@ +[general] +version = 2 +name = M5 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = -2 +quality_type = faster +setting_version = 3 + +[values] +layer_height = 0.21875 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg new file mode 100644 index 0000000000..881f70521f --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg @@ -0,0 +1,22 @@ +[general] +version = 2 +name = M6 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = -3 +quality_type = draft +setting_version = 3 + +[values] +layer_height = 0.2625 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg new file mode 100644 index 0000000000..5d81cbc259 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg @@ -0,0 +1,22 @@ +[general] +version = 2 +name = M7 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = -4 +quality_type = turbo +setting_version = 3 + +[values] +layer_height = 0.30625 +layer_height_0 = 0.30625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg new file mode 100644 index 0000000000..74294ea6f6 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = M8 Quality +definition = malyan_m200 + +[metadata] +type = quality +weight = -5 +quality_type = hyper +global_quality = true +setting_version = 3 + +[values] +layer_height = 0.35 +layer_height_0 = 0.35 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/variants/malyan_m200_0.15.inst.cfg b/resources/variants/malyan_m200_0.15.inst.cfg new file mode 100644 index 0000000000..808b8755ea --- /dev/null +++ b/resources/variants/malyan_m200_0.15.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.15 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.15 +machine_nozzle_tip_outer_diameter = 0.8 +coasting_volume = 0.05 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.2, 1) +speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) +speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.25.inst.cfg b/resources/variants/malyan_m200_0.25.inst.cfg new file mode 100644 index 0000000000..3847f5c617 --- /dev/null +++ b/resources/variants/malyan_m200_0.25.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.25 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.25 +machine_nozzle_tip_outer_diameter = 0.8 +coasting_volume = 0.1 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.2, 1) +speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) +speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.30.inst.cfg b/resources/variants/malyan_m200_0.30.inst.cfg new file mode 100644 index 0000000000..fee8aae40f --- /dev/null +++ b/resources/variants/malyan_m200_0.30.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.30 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.30 +machine_nozzle_tip_outer_diameter = 0.8 +coasting_volume = 0.11 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.2, 1) +speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) +speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.35.inst.cfg b/resources/variants/malyan_m200_0.35.inst.cfg new file mode 100644 index 0000000000..dcbb3ed4c0 --- /dev/null +++ b/resources/variants/malyan_m200_0.35.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.35 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.35 +machine_nozzle_tip_outer_diameter = 0.8 +coasting_volume = 0.13 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.2, 1) +speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) +speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.40.inst.cfg b/resources/variants/malyan_m200_0.40.inst.cfg new file mode 100644 index 0000000000..6e17dd13a6 --- /dev/null +++ b/resources/variants/malyan_m200_0.40.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.40 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.40 +machine_nozzle_tip_outer_diameter = 1.05 +coasting_volume = 0.15 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.25, 1) +speed_wall_0 = =1 if speed_wall < 10 else (speed_wall - 10) +speed_topbottom = =round(speed_print / 2.25, 1) diff --git a/resources/variants/malyan_m200_0.50.inst.cfg b/resources/variants/malyan_m200_0.50.inst.cfg new file mode 100644 index 0000000000..ce87def1e6 --- /dev/null +++ b/resources/variants/malyan_m200_0.50.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.50 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.50 +machine_nozzle_tip_outer_diameter = 0.8 +coasting_volume = 0.2 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.2, 1) +speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) +speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.60.inst.cfg b/resources/variants/malyan_m200_0.60.inst.cfg new file mode 100644 index 0000000000..0ee7c786e8 --- /dev/null +++ b/resources/variants/malyan_m200_0.60.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.60 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.60 +machine_nozzle_tip_outer_diameter = 1.25 +coasting_volume = 0.28 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print * 4 / 3, 1) +speed_wall_0 = =1 if speed_wall < 10 else (speed_wall - 10) +speed_topbottom = =round(speed_print / 2, 1) diff --git a/resources/variants/malyan_m200_0.80.inst.cfg b/resources/variants/malyan_m200_0.80.inst.cfg new file mode 100644 index 0000000000..54f48afdb4 --- /dev/null +++ b/resources/variants/malyan_m200_0.80.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 0.80 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 0.80 +machine_nozzle_tip_outer_diameter = 1.35 +coasting_volume = 0.45 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print * 4 / 3, 1) +speed_wall_0 = =1 if speed_wall < 10 else (speed_wall - 10) +speed_topbottom = =round(speed_print / 2, 1) diff --git a/resources/variants/malyan_m200_1.00.inst.cfg b/resources/variants/malyan_m200_1.00.inst.cfg new file mode 100644 index 0000000000..ce0a2d0dfb --- /dev/null +++ b/resources/variants/malyan_m200_1.00.inst.cfg @@ -0,0 +1,18 @@ +[general] +name = 1.00 mm +version = 2 +definition = malyan_m200 + +[metadata] +author = Brian Corbino +type = variant +setting_version = 3 + +[values] +machine_nozzle_size = 1.00 +machine_nozzle_tip_outer_diameter = 0.8 +coasting_volume = 0.63 +coasting_min_volume = =round(coasting_volume * 4,2) +speed_wall = =round(speed_print / 1.2, 1) +speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) +speed_topbottom = =round(speed_print / 1.5, 1) From 80f50dfff579b30f54eeee2205a72e735a8acf2f Mon Sep 17 00:00:00 2001 From: tylergibson Date: Wed, 13 Dec 2017 18:33:18 -0800 Subject: [PATCH 111/200] renaming variants to fix filename conflict, updating quality setting version --- resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg | 2 +- resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg | 2 +- .../{malyan_m200_0.15.inst.cfg => malyan_m200_0.15mm.inst.cfg} | 0 .../{malyan_m200_0.25.inst.cfg => malyan_m200_0.25mm.inst.cfg} | 0 .../{malyan_m200_0.30.inst.cfg => malyan_m200_0.30mm.inst.cfg} | 0 .../{malyan_m200_0.35.inst.cfg => malyan_m200_0.35mm.inst.cfg} | 0 .../{malyan_m200_0.40.inst.cfg => malyan_m200_0.40mm.inst.cfg} | 0 .../{malyan_m200_0.50.inst.cfg => malyan_m200_0.50mm.inst.cfg} | 0 .../{malyan_m200_0.60.inst.cfg => malyan_m200_0.60mm.inst.cfg} | 0 .../{malyan_m200_0.80.inst.cfg => malyan_m200_0.80mm.inst.cfg} | 0 .../{malyan_m200_1.00.inst.cfg => malyan_m200_1.00mm.inst.cfg} | 0 17 files changed, 8 insertions(+), 8 deletions(-) rename resources/variants/{malyan_m200_0.15.inst.cfg => malyan_m200_0.15mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.25.inst.cfg => malyan_m200_0.25mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.30.inst.cfg => malyan_m200_0.30mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.35.inst.cfg => malyan_m200_0.35mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.40.inst.cfg => malyan_m200_0.40mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.50.inst.cfg => malyan_m200_0.50mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.60.inst.cfg => malyan_m200_0.60mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_0.80.inst.cfg => malyan_m200_0.80mm.inst.cfg} (100%) rename resources/variants/{malyan_m200_1.00.inst.cfg => malyan_m200_1.00mm.inst.cfg} (100%) diff --git a/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg index c19d6f2050..54be6ecbcc 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.04375.inst.cfg @@ -7,7 +7,7 @@ definition = malyan_m200 type = quality weight = 2 quality_type = fine -setting_version = 3 +setting_version = 4 [values] layer_height = 0.04375 diff --git a/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg index 519e963227..568dd796f3 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.0875.inst.cfg @@ -7,7 +7,7 @@ definition = malyan_m200 type = quality weight = 1 quality_type = high -setting_version = 3 +setting_version = 4 [values] layer_height = 0.0875 diff --git a/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg index 732218cd79..1dc436502b 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.13125.inst.cfg @@ -7,7 +7,7 @@ definition = malyan_m200 type = quality weight = 0 quality_type = normal -setting_version = 3 +setting_version = 4 [values] layer_height = 0.13125 diff --git a/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg index f27e1ff900..314a8acd83 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.175.inst.cfg @@ -8,7 +8,7 @@ type = quality weight = -1 quality_type = fast global_quality = true -setting_version = 3 +setting_version = 4 [values] layer_height = 0.175 diff --git a/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg index 3ff3b562b2..a7fedb7e04 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.21875.inst.cfg @@ -7,7 +7,7 @@ definition = malyan_m200 type = quality weight = -2 quality_type = faster -setting_version = 3 +setting_version = 4 [values] layer_height = 0.21875 diff --git a/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg index 881f70521f..441abc3070 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.2625.inst.cfg @@ -7,7 +7,7 @@ definition = malyan_m200 type = quality weight = -3 quality_type = draft -setting_version = 3 +setting_version = 4 [values] layer_height = 0.2625 diff --git a/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg index 5d81cbc259..2588838174 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.30625.inst.cfg @@ -7,7 +7,7 @@ definition = malyan_m200 type = quality weight = -4 quality_type = turbo -setting_version = 3 +setting_version = 4 [values] layer_height = 0.30625 diff --git a/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg b/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg index 74294ea6f6..800b6104d9 100644 --- a/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg +++ b/resources/quality/malyan_m200/malyan_m200_0.35.inst.cfg @@ -8,7 +8,7 @@ type = quality weight = -5 quality_type = hyper global_quality = true -setting_version = 3 +setting_version = 4 [values] layer_height = 0.35 diff --git a/resources/variants/malyan_m200_0.15.inst.cfg b/resources/variants/malyan_m200_0.15mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.15.inst.cfg rename to resources/variants/malyan_m200_0.15mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.25.inst.cfg b/resources/variants/malyan_m200_0.25mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.25.inst.cfg rename to resources/variants/malyan_m200_0.25mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.30.inst.cfg b/resources/variants/malyan_m200_0.30mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.30.inst.cfg rename to resources/variants/malyan_m200_0.30mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.35.inst.cfg b/resources/variants/malyan_m200_0.35mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.35.inst.cfg rename to resources/variants/malyan_m200_0.35mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.40.inst.cfg b/resources/variants/malyan_m200_0.40mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.40.inst.cfg rename to resources/variants/malyan_m200_0.40mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.50.inst.cfg b/resources/variants/malyan_m200_0.50mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.50.inst.cfg rename to resources/variants/malyan_m200_0.50mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.60.inst.cfg b/resources/variants/malyan_m200_0.60mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.60.inst.cfg rename to resources/variants/malyan_m200_0.60mm.inst.cfg diff --git a/resources/variants/malyan_m200_0.80.inst.cfg b/resources/variants/malyan_m200_0.80mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_0.80.inst.cfg rename to resources/variants/malyan_m200_0.80mm.inst.cfg diff --git a/resources/variants/malyan_m200_1.00.inst.cfg b/resources/variants/malyan_m200_1.00mm.inst.cfg similarity index 100% rename from resources/variants/malyan_m200_1.00.inst.cfg rename to resources/variants/malyan_m200_1.00mm.inst.cfg From 93a3054af0582d0d6230f9d578161464ee64ba02 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 19 Dec 2017 10:38:36 +0100 Subject: [PATCH 112/200] Round layer heights to 2 decimals More difficult than I expected actually... Contributes to issue CURA-4655. --- resources/qml/SidebarSimple.qml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index 62cf6f9d34..76d8bb32df 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -183,12 +183,22 @@ Item text: { var result = "" - if(Cura.MachineManager.activeMachine != null){ - - var result = Cura.ProfilesModel.getItem(index).layer_height_without_unit + if(Cura.MachineManager.activeMachine != null) + { + result = Cura.ProfilesModel.getItem(index).layer_height_without_unit if(result == undefined) - result = "" + { + result = ""; + } + else + { + result = Number(Math.round(result + "e+2") + "e-2"); //Round to 2 decimals. Javascript makes this difficult... + if (result == undefined || result != result) //Parse failure. + { + result = ""; + } + } } return result } From 5e3666f073504e08da1c9ffbc7072831bd0452dc Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Tue, 2 Jan 2018 17:21:51 +0100 Subject: [PATCH 113/200] 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 114/200] 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 115/200] 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 116/200] 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 117/200] 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 118/200] 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 119/200] 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 120/200] 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 121/200] 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 122/200] 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 123/200] 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 124/200] 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 125/200] 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 126/200] 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 127/200] 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 128/200] 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 129/200] 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 130/200] 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 131/200] 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 5dad6cab2c546d0d04ea92f5b909be912f4282d6 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Fri, 5 Jan 2018 14:00:23 +0100 Subject: [PATCH 132/200] If the model is:some per mesh settings or outside of the buildplate show message, CURA-4525 --- plugins/CuraEngineBackend/CuraEngineBackend.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 4491008997..44c39db69f 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -357,6 +357,18 @@ class CuraEngineBackend(QObject, Backend): else: self.backendStateChange.emit(BackendState.NotStarted) + if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: + if Application.getInstance().platformActivity: + self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), + title = catalog.i18nc("@info:title", "Unable to slice")) + self._error_message.show() + self.backendStateChange.emit(BackendState.Error) + else: + self.backendStateChange.emit(BackendState.NotStarted) + pass + self._invokeSlice() + return + # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) From 39921795cee3e79d74267cd2092ecea4c0fee97a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 5 Jan 2018 14:46:16 +0100 Subject: [PATCH 133/200] 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 784a17149f48da575a7b8f5a2e1be4e78a9d1a69 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Fri, 5 Jan 2018 15:20:42 +0100 Subject: [PATCH 134/200] Don't show save to file if not possible to slice CURA-4525 --- plugins/CuraEngineBackend/CuraEngineBackend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 44c39db69f..0ca500ecec 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -198,7 +198,6 @@ class CuraEngineBackend(QObject, Backend): self._slice_start_time = time() if not self._build_plates_to_be_sliced: self.processingProgress.emit(1.0) - self.backendStateChange.emit(BackendState.Done) Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") return From 1a520a041b0719c86db91d12e3e3fb8df9bef18c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 5 Jan 2018 16:27:57 +0100 Subject: [PATCH 135/200] CURA-4768 Allow groups of models to be included in the select all and arrange tools. --- cura/CuraApplication.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 476b5d3708..8aeeb9c1e8 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1020,7 +1020,7 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data - if not node.callDecoration("isSliceable"): + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): continue # i.e. node with layer data Selection.add(node) @@ -1092,7 +1092,7 @@ class CuraApplication(QtApplication): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.callDecoration("isSliceable"): + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): continue # i.e. node with layer data nodes.append(node) @@ -1119,7 +1119,7 @@ class CuraApplication(QtApplication): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) - if not node.callDecoration("isSliceable"): + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): continue # i.e. node with layer data # Skip nodes that are too big if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: @@ -1142,7 +1142,7 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data - if not node.callDecoration("isSliceable"): + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): continue # i.e. node with layer data if node.callDecoration("getBuildPlateNumber") == active_build_plate: # Skip nodes that are too big @@ -1166,7 +1166,7 @@ class CuraApplication(QtApplication): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data - if not node.callDecoration("isSliceable"): + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): continue # i.e. node with layer data if node in nodes: # exclude selected node from fixed_nodes continue From c839bc175998707ec1ddddb4464298ff2884d812 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 5 Jan 2018 16:53:18 +0100 Subject: [PATCH 136/200] Groups of objects are taken into account and are shown in the object list instead of each model separatelly. Contributes to CURA-4525 --- cura/ObjectsModel.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index 2e83ee9033..7efbb8e5b9 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -24,16 +24,28 @@ class ObjectsModel(ListModel): nodes = [] filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") active_build_plate_number = self._build_plate_number + group_nr = 1 for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): - if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): + if not issubclass(type(node), SceneNode): continue - if not node.callDecoration("isSliceable"): + if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): + continue + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): continue node_build_plate_number = node.callDecoration("getBuildPlateNumber") if filter_current_build_plate and node_build_plate_number != active_build_plate_number: continue + + if not node.callDecoration("isGroup"): + name = node.getName() + else: + name = "Group #" + str(group_nr) + group_nr += 1 + nodes.append({ - "name": node.getName(), + "name": name, "isSelected": Selection.isSelected(node), "isOutsideBuildArea": node.isOutsideBuildArea(), "buildPlateNumber": node_build_plate_number, From ab2450630701714f85d522214260e3574bc06931 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 5 Jan 2018 17:05:11 +0100 Subject: [PATCH 137/200] Add string to cura I18N catalog. Contributes to CURA-4525 --- cura/ObjectsModel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index 7efbb8e5b9..5218127fc5 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -4,7 +4,9 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from UM.Preferences import Preferences +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") ## Keep track of all objects in the project class ObjectsModel(ListModel): @@ -41,7 +43,7 @@ class ObjectsModel(ListModel): if not node.callDecoration("isGroup"): name = node.getName() else: - name = "Group #" + str(group_nr) + name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr)) group_nr += 1 nodes.append({ From 7ad15c7dd2771bdf1815ec78126b58ae4071348d Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 5 Jan 2018 17:25:04 +0100 Subject: [PATCH 138/200] Allow adding support-specific settings for support meshes in Per Model Settings --- .../PerObjectSettingsPanel.qml | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 5bdb6d4cb0..b4e7a46e00 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -18,6 +18,9 @@ Item { width: childrenRect.width; height: childrenRect.height; + property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed", + "travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"] + Column { id: items @@ -106,7 +109,7 @@ Item { id: currentSettings property int maximumHeight: 200 * screenScaleFactor height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight) - visible: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1 + visible: meshTypeSelection.model.get(meshTypeSelection.currentIndex).type != "anti_overhang_mesh" ScrollView { @@ -124,7 +127,15 @@ Item { id: addedSettingsModel; containerId: Cura.MachineManager.activeDefinitionId expanded: [ "*" ] - exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ] + exclude: { + var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]; + + if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh") + { + excluded_settings = excluded_settings.concat(base.all_categories_except_support); + } + return excluded_settings; + } visibilityHandler: Cura.PerObjectSettingVisibilityHandler { @@ -306,7 +317,18 @@ Item { } } - onClicked: settingPickDialog.visible = true; + onClicked: + { + settingPickDialog.visible = true; + if (meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh") + { + settingPickDialog.additional_excluded_settings = base.all_categories_except_support; + } + else + { + settingPickDialog.additional_excluded_settings = [] + } + } } } @@ -315,9 +337,10 @@ Item { id: settingPickDialog title: catalog.i18nc("@title:window", "Select Settings to Customize for this model") - width: screenScaleFactor * 360; + width: screenScaleFactor * 360 property string labelFilter: "" + property var additional_excluded_settings onVisibilityChanged: { @@ -394,7 +417,12 @@ Item { } visibilityHandler: UM.SettingPreferenceVisibilityHandler {} expanded: [ "*" ] - exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ] + exclude: + { + var excluded_settings = [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]; + excluded_settings = excluded_settings.concat(settingPickDialog.additional_excluded_settings); + return excluded_settings; + } } delegate:Loader { From 8bd6afad1fe5a5dc0a1de8b2ee5bbcc4deb4d158 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 5 Jan 2018 18:33:05 +0100 Subject: [PATCH 139/200] Render support meshes with a vertical stripe... ...so they are easier to tell apart from normal meshes, especially for single extrusion printers --- plugins/SolidView/SolidView.py | 16 ++++++++++++++++ resources/shaders/striped.shader | 14 +++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index e156e655ce..50ff2864b7 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -28,6 +28,7 @@ class SolidView(View): self._enabled_shader = None self._disabled_shader = None self._non_printing_shader = None + self._support_mesh_shader = None self._extruders_model = ExtrudersModel() self._theme = None @@ -54,6 +55,11 @@ class SolidView(View): self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb())) self._non_printing_shader.setUniformValue("u_opacity", 0.6) + if not self._support_mesh_shader: + self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader")) + self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) + self._support_mesh_shader.setUniformValue("u_width", 5.0) + global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value") @@ -117,6 +123,16 @@ class SolidView(View): renderer.queueNode(node, shader = self._non_printing_shader, transparent = True) elif getattr(node, "_outside_buildarea", False): renderer.queueNode(node, shader = self._disabled_shader) + elif per_mesh_stack and per_mesh_stack.getProperty("support_mesh", "value"): + # Render support meshes with a vertical stripe that is darker + shade_factor = 0.6 + uniforms["diffuse_color_2"] = [ + uniforms["diffuse_color"][0] * shade_factor, + uniforms["diffuse_color"][1] * shade_factor, + uniforms["diffuse_color"][2] * shade_factor, + 1.0 + ] + renderer.queueNode(node, shader = self._support_mesh_shader, uniforms = uniforms) else: renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms) if node.callDecoration("isGroup") and Selection.isSelected(node): diff --git a/resources/shaders/striped.shader b/resources/shaders/striped.shader index ce7d14e39e..7cf5a62c3f 100644 --- a/resources/shaders/striped.shader +++ b/resources/shaders/striped.shader @@ -32,6 +32,7 @@ fragment = uniform highp vec3 u_viewPosition; uniform mediump float u_width; + uniform mediump bool u_vertical_stripes; varying highp vec3 v_position; varying highp vec3 v_vertex; @@ -40,7 +41,9 @@ fragment = void main() { mediump vec4 finalColor = vec4(0.0); - mediump vec4 diffuseColor = (mod((-v_position.x + v_position.y), u_width) < (u_width / 2.)) ? u_diffuseColor1 : u_diffuseColor2; + mediump vec4 diffuseColor = u_vertical_stripes ? + (((mod(v_vertex.x, u_width) < (u_width / 2.)) ^^ (mod(v_vertex.z, u_width) < (u_width / 2.))) ? u_diffuseColor1 : u_diffuseColor2) : + ((mod((-v_position.x + v_position.y), u_width) < (u_width / 2.)) ? u_diffuseColor1 : u_diffuseColor2); /* Ambient Component */ finalColor += u_ambientColor; @@ -98,6 +101,7 @@ fragment41core = uniform highp vec3 u_viewPosition; uniform mediump float u_width; + uniform mediump bool u_vertical_stripes; in highp vec3 v_position; in highp vec3 v_vertex; @@ -108,7 +112,9 @@ fragment41core = void main() { mediump vec4 finalColor = vec4(0.0); - mediump vec4 diffuseColor = (mod((-v_position.x + v_position.y), u_width) < (u_width / 2.)) ? u_diffuseColor1 : u_diffuseColor2; + mediump vec4 diffuseColor = u_vertical_stripes ? + (((mod(v_vertex.x, u_width) < (u_width / 2.)) ^^ (mod(v_vertex.z, u_width) < (u_width / 2.))) ? u_diffuseColor1 : u_diffuseColor2) : + ((mod((-v_position.x + v_position.y), u_width) < (u_width / 2.)) ? u_diffuseColor1 : u_diffuseColor2); /* Ambient Component */ finalColor += u_ambientColor; @@ -138,6 +144,7 @@ u_diffuseColor2 = [0.5, 0.5, 0.5, 1.0] u_specularColor = [0.4, 0.4, 0.4, 1.0] u_shininess = 20.0 u_width = 5.0 +u_vertical_stripes = 0 [bindings] u_modelMatrix = model_matrix @@ -145,7 +152,8 @@ u_viewProjectionMatrix = view_projection_matrix u_normalMatrix = normal_matrix u_viewPosition = view_position u_lightPosition = light_0_position -u_diffuseColor = diffuse_color +u_diffuseColor1 = diffuse_color +u_diffuseColor2 = diffuse_color_2 [attributes] a_vertex = vertex From fbe128bbb75bb52f4fe9a2234c22de57687b7512 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 5 Jan 2018 21:38:54 +0100 Subject: [PATCH 140/200] Hide options for Support, Anti overhang, etc Mesh if their settings are disabled --- .../PerObjectSettingsPanel.qml | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index b4e7a46e00..a38112724e 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -42,6 +42,13 @@ Item { verticalAlignment: Text.AlignVCenter } + UM.SettingPropertyProvider + { + id: meshTypePropertyProvider + containerStackId: Cura.MachineManager.activeMachineId + watchedProperties: [ "enabled" ] + } + ComboBox { id: meshTypeSelection @@ -52,36 +59,55 @@ Item { model: ListModel { id: meshTypeModel - Component.onCompleted: + Component.onCompleted: meshTypeSelection.populateModel() + } + + function populateModel() + { + meshTypeModel.append({ + type: "", + text: catalog.i18nc("@label", "Normal model") + }); + meshTypePropertyProvider.key = "support_mesh"; + if(meshTypePropertyProvider.properties.enabled == "True") { - meshTypeModel.append({ - type: "", - text: catalog.i18nc("@label", "Normal model") - }); meshTypeModel.append({ type: "support_mesh", text: catalog.i18nc("@label", "Print as support") }); + } + meshTypePropertyProvider.key = "anti_overhang_mesh"; + if(meshTypePropertyProvider.properties.enabled == "True") + { meshTypeModel.append({ type: "anti_overhang_mesh", text: catalog.i18nc("@label", "Don't support overlap with other models") }); + } + meshTypePropertyProvider.key = "cutting_mesh"; + if(meshTypePropertyProvider.properties.enabled == "True") + { meshTypeModel.append({ type: "cutting_mesh", text: catalog.i18nc("@label", "Modify settings for overlap with other models") }); + } + meshTypePropertyProvider.key = "infill_mesh"; + if(meshTypePropertyProvider.properties.enabled == "True") + { meshTypeModel.append({ type: "infill_mesh", text: catalog.i18nc("@label", "Modify settings for infill of other models") }); - - meshTypeSelection.updateCurrentIndex(); } + + meshTypeSelection.updateCurrentIndex(); } function updateCurrentIndex() { var mesh_type = UM.ActiveTool.properties.getValue("MeshType"); + meshTypeSelection.currentIndex = -1; for(var index=0; index < meshTypeSelection.model.count; index++) { if(meshTypeSelection.model.get(index).type == mesh_type) @@ -94,6 +120,16 @@ Item { } } + Connections + { + target: Cura.MachineManager + onGlobalContainerChanged: + { + meshTypeSelection.model.clear(); + meshTypeSelection.populateModel(); + } + } + Connections { target: UM.Selection From 8dd5b166f439b5d0f2d5282baea468134343f576 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sat, 6 Jan 2018 22:46:59 +0100 Subject: [PATCH 141/200] Move 'Drop Down Support Mesh" to "Support" section... ... so it can be added as a per model setting for Support Meshes --- resources/definitions/fdmprinter.def.json | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 6eef6b1e9b..08c8b75869 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -4233,6 +4233,18 @@ "limit_to_extruder": "support_infill_extruder_nr", "enabled": "support_enable and support_use_towers", "settable_per_mesh": true + }, + "support_mesh_drop_down": + { + "label": "Drop Down Support Mesh", + "description": "Make support everywhere below the support mesh, so that there's no overhang in the support mesh.", + "type": "bool", + "default_value": true, + "enabled": "support_mesh", + "settable_per_mesh": true, + "settable_per_extruder": false, + "settable_per_meshgroup": false, + "settable_globally": false } } }, @@ -5261,18 +5273,6 @@ "settable_per_meshgroup": false, "settable_globally": false }, - "support_mesh_drop_down": - { - "label": "Drop Down Support Mesh", - "description": "Make support everywhere below the support mesh, so that there's no overhang in the support mesh.", - "type": "bool", - "default_value": true, - "enabled": "support_mesh", - "settable_per_mesh": true, - "settable_per_extruder": false, - "settable_per_meshgroup": false, - "settable_globally": false - }, "anti_overhang_mesh": { "label": "Anti Overhang Mesh", From 3708c37b6022af916bd1ac60868d144eb0abf284 Mon Sep 17 00:00:00 2001 From: Anton Midyukov Date: Sun, 7 Jan 2018 17:17:32 +0700 Subject: [PATCH 142/200] Activation workaround Linux+NVidia proprietary drivers for all distributions --- cura_app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cura_app.py b/cura_app.py index b5844055ab..6d1ff6ab6b 100755 --- a/cura_app.py +++ b/cura_app.py @@ -40,11 +40,11 @@ import faulthandler if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 linux_distro_name = platform.linux_distribution()[0].lower() - if linux_distro_name in ("debian", "ubuntu", "linuxmint", "fedora"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix. - import ctypes - from ctypes.util import find_library - libGL = find_library("GL") - ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL) + # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix. + import ctypes + from ctypes.util import find_library + libGL = find_library("GL") + ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL) # When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs. if Platform.isWindows() and hasattr(sys, "frozen"): From c885341e5a3b88e865db8d8de86fc7f3097622f3 Mon Sep 17 00:00:00 2001 From: Tyler Gibson Date: Sun, 7 Jan 2018 22:39:54 -0800 Subject: [PATCH 143/200] Removing nozzle variants, adding material definitions --- resources/definitions/malyan_m200.def.json | 17 +++++--------- .../monoprice_select_mini_v1.def.json | 2 +- .../monoprice_select_mini_v2.def.json | 9 +++++--- .../abs/malyan_m200_abs_draft.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_fast.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_high.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_normal.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_superdraft.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_thickerdraft.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_ultra.inst.cfg | 15 ++++++++++++ .../abs/malyan_m200_abs_verydraft.inst.cfg | 15 ++++++++++++ .../malyan_m200_global_Draft_Quality.inst.cfg | 23 +++++++++++++++++++ .../malyan_m200_global_Fast_Quality.inst.cfg | 23 +++++++++++++++++++ .../malyan_m200_global_High_Quality.inst.cfg | 23 +++++++++++++++++++ ...malyan_m200_global_Normal_Quality.inst.cfg | 23 +++++++++++++++++++ ...an_m200_global_SuperDraft_Quality.inst.cfg | 23 +++++++++++++++++++ ..._m200_global_ThickerDraft_Quality.inst.cfg | 23 +++++++++++++++++++ .../malyan_m200_global_Ultra_Quality.inst.cfg | 23 +++++++++++++++++++ ...yan_m200_global_VeryDraft_Quality.inst.cfg | 23 +++++++++++++++++++ .../petg/malyan_m200_petg_draft.inst.cfg | 11 +++++++++ .../petg/malyan_m200_petg_fast.inst.cfg | 11 +++++++++ .../petg/malyan_m200_petg_high.inst.cfg | 11 +++++++++ .../petg/malyan_m200_petg_normal.inst.cfg | 11 +++++++++ .../petg/malyan_m200_petg_superdraft.inst.cfg | 11 +++++++++ .../malyan_m200_petg_thickerdraft.inst.cfg | 11 +++++++++ .../petg/malyan_m200_petg_ultra.inst.cfg | 11 +++++++++ .../petg/malyan_m200_petg_verydraft.inst.cfg | 11 +++++++++ .../pla/malyan_m200_pla_draft.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_fast.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_high.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_normal.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_superdraft.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_thickerdraft.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_ultra.inst.cfg | 15 ++++++++++++ .../pla/malyan_m200_pla_verydraft.inst.cfg | 15 ++++++++++++ ...onoprice_select_mini_v2_abs_draft.inst.cfg | 15 ++++++++++++ ...monoprice_select_mini_v2_abs_fast.inst.cfg | 15 ++++++++++++ ...monoprice_select_mini_v2_abs_high.inst.cfg | 15 ++++++++++++ ...noprice_select_mini_v2_abs_normal.inst.cfg | 15 ++++++++++++ ...ice_select_mini_v2_abs_superdraft.inst.cfg | 15 ++++++++++++ ...e_select_mini_v2_abs_thickerdraft.inst.cfg | 15 ++++++++++++ ...onoprice_select_mini_v2_abs_ultra.inst.cfg | 15 ++++++++++++ ...rice_select_mini_v2_abs_verydraft.inst.cfg | 15 ++++++++++++ ...lect_mini_v2_global_Draft_Quality.inst.cfg | 23 +++++++++++++++++++ ...elect_mini_v2_global_Fast_Quality.inst.cfg | 23 +++++++++++++++++++ ...elect_mini_v2_global_High_Quality.inst.cfg | 23 +++++++++++++++++++ ...ect_mini_v2_global_Normal_Quality.inst.cfg | 23 +++++++++++++++++++ ...mini_v2_global_SuperDraft_Quality.inst.cfg | 23 +++++++++++++++++++ ...ni_v2_global_ThickerDraft_Quality.inst.cfg | 23 +++++++++++++++++++ ...lect_mini_v2_global_Ultra_Quality.inst.cfg | 23 +++++++++++++++++++ ..._mini_v2_global_VeryDraft_Quality.inst.cfg | 23 +++++++++++++++++++ ...oprice_select_mini_v2_nylon_draft.inst.cfg | 11 +++++++++ ...noprice_select_mini_v2_nylon_fast.inst.cfg | 11 +++++++++ ...noprice_select_mini_v2_nylon_high.inst.cfg | 11 +++++++++ ...price_select_mini_v2_nylon_normal.inst.cfg | 11 +++++++++ ...e_select_mini_v2_nylon_superdraft.inst.cfg | 11 +++++++++ ...select_mini_v2_nylon_thickerdraft.inst.cfg | 11 +++++++++ ...oprice_select_mini_v2_nylon_ultra.inst.cfg | 11 +++++++++ ...ce_select_mini_v2_nylon_verydraft.inst.cfg | 11 +++++++++ ...monoprice_select_mini_v2_pc_draft.inst.cfg | 15 ++++++++++++ .../monoprice_select_mini_v2_pc_fast.inst.cfg | 15 ++++++++++++ .../monoprice_select_mini_v2_pc_high.inst.cfg | 15 ++++++++++++ ...onoprice_select_mini_v2_pc_normal.inst.cfg | 15 ++++++++++++ ...rice_select_mini_v2_pc_superdraft.inst.cfg | 15 ++++++++++++ ...ce_select_mini_v2_pc_thickerdraft.inst.cfg | 15 ++++++++++++ ...monoprice_select_mini_v2_pc_ultra.inst.cfg | 15 ++++++++++++ ...price_select_mini_v2_pc_verydraft.inst.cfg | 15 ++++++++++++ ...noprice_select_mini_v2_petg_draft.inst.cfg | 11 +++++++++ ...onoprice_select_mini_v2_petg_fast.inst.cfg | 11 +++++++++ ...onoprice_select_mini_v2_petg_high.inst.cfg | 11 +++++++++ ...oprice_select_mini_v2_petg_normal.inst.cfg | 11 +++++++++ ...ce_select_mini_v2_petg_superdraft.inst.cfg | 11 +++++++++ ..._select_mini_v2_petg_thickerdraft.inst.cfg | 11 +++++++++ ...noprice_select_mini_v2_petg_ultra.inst.cfg | 11 +++++++++ ...ice_select_mini_v2_petg_verydraft.inst.cfg | 11 +++++++++ ...onoprice_select_mini_v2_pla_draft.inst.cfg | 11 +++++++++ ...monoprice_select_mini_v2_pla_fast.inst.cfg | 11 +++++++++ ...monoprice_select_mini_v2_pla_high.inst.cfg | 11 +++++++++ ...noprice_select_mini_v2_pla_normal.inst.cfg | 11 +++++++++ ...ice_select_mini_v2_pla_superdraft.inst.cfg | 11 +++++++++ ...e_select_mini_v2_pla_thickerdraft.inst.cfg | 11 +++++++++ ...onoprice_select_mini_v2_pla_ultra.inst.cfg | 11 +++++++++ ...rice_select_mini_v2_pla_verydraft.inst.cfg | 11 +++++++++ .../variants/malyan_m200_0.15mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.25mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.30mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.35mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.40mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.50mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.60mm.inst.cfg | 18 --------------- .../variants/malyan_m200_0.80mm.inst.cfg | 18 --------------- .../variants/malyan_m200_1.00mm.inst.cfg | 18 --------------- 92 files changed, 1213 insertions(+), 177 deletions(-) create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_draft.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_fast.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_high.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_normal.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_superdraft.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_thickerdraft.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_ultra.inst.cfg create mode 100644 resources/quality/malyan_m200/abs/malyan_m200_abs_verydraft.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_Draft_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_Fast_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_High_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_Normal_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_SuperDraft_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_ThickerDraft_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_Ultra_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/malyan_m200_global_VeryDraft_Quality.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_draft.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_fast.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_high.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_normal.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_superdraft.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_thickerdraft.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_ultra.inst.cfg create mode 100644 resources/quality/malyan_m200/petg/malyan_m200_petg_verydraft.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_draft.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_fast.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_high.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_normal.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_superdraft.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_thickerdraft.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_ultra.inst.cfg create mode 100644 resources/quality/malyan_m200/pla/malyan_m200_pla_verydraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_draft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_fast.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_high.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_normal.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_superdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_thickerdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_ultra.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_verydraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Draft_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Fast_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_High_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Normal_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_SuperDraft_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_ThickerDraft_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Ultra_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_VeryDraft_Quality.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_draft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_fast.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_high.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_normal.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_superdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_thickerdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_ultra.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_verydraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_draft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_fast.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_high.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_normal.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_superdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_thickerdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_ultra.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_verydraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_draft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_fast.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_high.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_normal.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_superdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_thickerdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_ultra.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_verydraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_draft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_fast.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_high.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_normal.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_superdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_thickerdraft.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_ultra.inst.cfg create mode 100644 resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_verydraft.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.15mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.25mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.30mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.35mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.40mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.50mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.60mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_0.80mm.inst.cfg delete mode 100644 resources/variants/malyan_m200_1.00mm.inst.cfg diff --git a/resources/definitions/malyan_m200.def.json b/resources/definitions/malyan_m200.def.json index f19980a3b7..9aae3a5244 100644 --- a/resources/definitions/malyan_m200.def.json +++ b/resources/definitions/malyan_m200.def.json @@ -4,19 +4,14 @@ "name": "Malyan M200", "inherits": "fdmprinter", "metadata": { - "author": "Brian Corbino", + "author": "Brian Corbino, Tyler Gibson", "manufacturer": "Malyan", "category": "Other", "file_formats": "text/x-gcode", "platform": "malyan_m200_platform.stl", - "has_variants": true, - "has_variant_materials": false, - "has_materials": true, - "has_machine_materials": false, "has_machine_quality": true, - "preferred_variant": "*0.4*", - "preferred_quality": "*0.175*", - "variants_name": "Nozzle size", + "has_materials": true, + "preferred_quality": "*normal*", "supports_usb_connection": true, "visible": true, "first_start_actions": ["MachineSettingsAction"], @@ -39,9 +34,9 @@ "line_width": { "value": "round(machine_nozzle_size * 0.875, 2)" }, "material_print_temperature": { "minimum_value": "0" }, - "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, + "material_print_temperature_layer_0": { "value": "min(material_print_temperature + 5, 245)" }, "material_bed_temperature": { "minimum_value": "0" }, - "material_bed_temperature_layer_0": { "value": "material_bed_temperature + 5" }, + "material_bed_temperature_layer_0": { "value": "min(material_bed_temperature + 5, 70)" }, "material_standby_temperature": { "minimum_value": "0" }, "machine_show_variants": { "default_value": true }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, @@ -59,7 +54,7 @@ "material_diameter": { "value": 1.75 }, "machine_nozzle_size": { "default_value": 0.4, - "minimum_value": "0.15" + "minimum_value": 0.15 }, "machine_max_feedrate_x": { "default_value": 150 }, "machine_max_feedrate_y": { "default_value": 150 }, diff --git a/resources/definitions/monoprice_select_mini_v1.def.json b/resources/definitions/monoprice_select_mini_v1.def.json index 0b76a26c1c..7264f0a6fc 100644 --- a/resources/definitions/monoprice_select_mini_v1.def.json +++ b/resources/definitions/monoprice_select_mini_v1.def.json @@ -4,7 +4,7 @@ "name": "Monoprice Select Mini V1", "inherits": "malyan_m200", "metadata": { - "author": "Brian Corbino", + "author": "Brian Corbino, Tyler Gibson", "manufacturer": "Monoprice", "category": "Other", "file_formats": "text/x-gcode", diff --git a/resources/definitions/monoprice_select_mini_v2.def.json b/resources/definitions/monoprice_select_mini_v2.def.json index 33367aad88..a935cb5a73 100644 --- a/resources/definitions/monoprice_select_mini_v2.def.json +++ b/resources/definitions/monoprice_select_mini_v2.def.json @@ -4,11 +4,13 @@ "name": "Monoprice Select Mini V2 (E3D)", "inherits": "malyan_m200", "metadata": { - "author": "Brian Corbino", + "author": "Tyler Gibson", "manufacturer": "Monoprice", "category": "Other", "file_formats": "text/x-gcode", - "quality_definition": "malyan_m200", + "has_machine_quality": true, + "has_materials": true, + "preferred_quality": "*normal*", "visible": true }, @@ -17,6 +19,7 @@ "adhesion_type": { "default_value": "brim" }, "retraction_combing": { "default_value": "noskin" }, "retraction_amount" : { "default_value": 2.5}, - "retraction_speed" : { "default_value": 40} + "retraction_speed" : { "default_value": 40}, + "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, } } diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_draft.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_draft.inst.cfg new file mode 100644 index 0000000000..19cc9fd00d --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_draft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fast +definition = malyan_m200 + +[metadata] +type = quality +quality_type = draft +material = generic_abs_175 +weight = -2 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_fast.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_fast.inst.cfg new file mode 100644 index 0000000000..5677a0d58d --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_fast.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Normal +definition = malyan_m200 + +[metadata] +type = quality +quality_type = fast +material = generic_abs_175 +weight = -1 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_high.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_high.inst.cfg new file mode 100644 index 0000000000..7798b3f545 --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_high.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Finer +definition = malyan_m200 + +[metadata] +type = quality +quality_type = high +material = generic_abs_175 +weight = 1 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_normal.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_normal.inst.cfg new file mode 100644 index 0000000000..c87c66c813 --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_normal.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fine +definition = malyan_m200 + +[metadata] +type = quality +quality_type = normal +material = generic_abs_175 +weight = 0 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_superdraft.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_superdraft.inst.cfg new file mode 100644 index 0000000000..e6e3cfcd6c --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_superdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = superdraft +material = generic_abs_175 +weight = -5 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_thickerdraft.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_thickerdraft.inst.cfg new file mode 100644 index 0000000000..fb08013809 --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_thickerdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_abs_175 +weight = -3 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_ultra.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_ultra.inst.cfg new file mode 100644 index 0000000000..385d852688 --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_ultra.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Ultra Fine +definition = malyan_m200 + +[metadata] +type = quality +quality_type = ultra +material = generic_abs_175 +weight = 2 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/abs/malyan_m200_abs_verydraft.inst.cfg b/resources/quality/malyan_m200/abs/malyan_m200_abs_verydraft.inst.cfg new file mode 100644 index 0000000000..7026391fb6 --- /dev/null +++ b/resources/quality/malyan_m200/abs/malyan_m200_abs_verydraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Low Detail Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = verydraft +material = generic_abs_175 +weight = -4 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/malyan_m200/malyan_m200_global_Draft_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_Draft_Quality.inst.cfg new file mode 100644 index 0000000000..d3104caa87 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_Draft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Fast +definition = malyan_m200 + +[metadata] +type = quality +weight = -2 +quality_type = draft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.21875 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_Fast_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_Fast_Quality.inst.cfg new file mode 100644 index 0000000000..aec535bd71 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_Fast_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Normal +definition = malyan_m200 + +[metadata] +type = quality +weight = -1 +quality_type = fast +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.175 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_High_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_High_Quality.inst.cfg new file mode 100644 index 0000000000..ca202862a2 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_High_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Finer +definition = malyan_m200 + +[metadata] +type = quality +weight = 1 +quality_type = high +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.0875 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_Normal_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_Normal_Quality.inst.cfg new file mode 100644 index 0000000000..7076718903 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_Normal_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Fine +definition = malyan_m200 + +[metadata] +type = quality +weight = 0 +quality_type = normal +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.13125 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_SuperDraft_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_SuperDraft_Quality.inst.cfg new file mode 100644 index 0000000000..7dfbdb5886 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_SuperDraft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = malyan_m200 + +[metadata] +type = quality +weight = -5 +quality_type = superdraft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.35 +layer_height_0 = 0.35 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_ThickerDraft_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_ThickerDraft_Quality.inst.cfg new file mode 100644 index 0000000000..2fbf82b128 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_ThickerDraft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Draft +definition = malyan_m200 + +[metadata] +type = quality +weight = -3 +quality_type = thickerdraft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.2625 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_Ultra_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_Ultra_Quality.inst.cfg new file mode 100644 index 0000000000..90e589cca5 --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_Ultra_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Ultra Fine +definition = malyan_m200 + +[metadata] +type = quality +weight = 2 +quality_type = ultra +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.04375 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/malyan_m200_global_VeryDraft_Quality.inst.cfg b/resources/quality/malyan_m200/malyan_m200_global_VeryDraft_Quality.inst.cfg new file mode 100644 index 0000000000..1210ee214b --- /dev/null +++ b/resources/quality/malyan_m200/malyan_m200_global_VeryDraft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Low Detail Draft +definition = malyan_m200 + +[metadata] +type = quality +weight = -4 +quality_type = verydraft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.30625 +layer_height_0 = 0.30625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_draft.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_draft.inst.cfg new file mode 100644 index 0000000000..aef83471ba --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_draft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fast +definition = malyan_m200 + +[metadata] +type = quality +quality_type = draft +material = generic_petg_175 +weight = -2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_fast.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_fast.inst.cfg new file mode 100644 index 0000000000..3c7fc2c239 --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_fast.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Normal +definition = malyan_m200 + +[metadata] +type = quality +quality_type = fast +material = generic_petg_175 +weight = -1 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_high.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_high.inst.cfg new file mode 100644 index 0000000000..eb1654eae3 --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_high.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Finer +definition = malyan_m200 + +[metadata] +type = quality +quality_type = high +material = generic_petg_175 +weight = 1 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_normal.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_normal.inst.cfg new file mode 100644 index 0000000000..53e60d2d62 --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_normal.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fine +definition = malyan_m200 + +[metadata] +type = quality +quality_type = normal +material = generic_petg_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_superdraft.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_superdraft.inst.cfg new file mode 100644 index 0000000000..d2a96386ae --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_superdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = superdraft +material = generic_petg_175 +weight = -5 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_thickerdraft.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_thickerdraft.inst.cfg new file mode 100644 index 0000000000..e2f37ae43b --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_thickerdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_petg_175 +weight = -3 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_ultra.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_ultra.inst.cfg new file mode 100644 index 0000000000..0fa89f2569 --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_ultra.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Ultra Fine +definition = malyan_m200 + +[metadata] +type = quality +quality_type = ultra +material = generic_petg_175 +weight = 2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/petg/malyan_m200_petg_verydraft.inst.cfg b/resources/quality/malyan_m200/petg/malyan_m200_petg_verydraft.inst.cfg new file mode 100644 index 0000000000..84bedf5c14 --- /dev/null +++ b/resources/quality/malyan_m200/petg/malyan_m200_petg_verydraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Low Detail Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = verydraft +material = generic_petg_175 +weight = -4 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_draft.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_draft.inst.cfg new file mode 100644 index 0000000000..4f221eceb7 --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_draft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fast +definition = malyan_m200 + +[metadata] +type = quality +quality_type = draft +material = generic_pla_175 +weight = -2 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_fast.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_fast.inst.cfg new file mode 100644 index 0000000000..3097fe055a --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_fast.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Normal +definition = malyan_m200 + +[metadata] +type = quality +quality_type = fast +material = generic_pla_175 +weight = -1 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_high.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_high.inst.cfg new file mode 100644 index 0000000000..062c120ad0 --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_high.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Finer +definition = malyan_m200 + +[metadata] +type = quality +quality_type = high +material = generic_pla_175 +weight = 1 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_normal.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_normal.inst.cfg new file mode 100644 index 0000000000..e01141ed9e --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_normal.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fine +definition = malyan_m200 + +[metadata] +type = quality +quality_type = normal +material = generic_pla_175 +weight = 0 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_superdraft.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_superdraft.inst.cfg new file mode 100644 index 0000000000..53eb4380eb --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_superdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = superdraft +material = generic_pla_175 +weight = -5 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_thickerdraft.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_thickerdraft.inst.cfg new file mode 100644 index 0000000000..32d2b419bc --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_thickerdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_pla_175 +weight = -3 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_ultra.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_ultra.inst.cfg new file mode 100644 index 0000000000..3865059254 --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_ultra.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Ultra Fine +definition = malyan_m200 + +[metadata] +type = quality +quality_type = ultra +material = generic_pla_175 +weight = 2 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/malyan_m200/pla/malyan_m200_pla_verydraft.inst.cfg b/resources/quality/malyan_m200/pla/malyan_m200_pla_verydraft.inst.cfg new file mode 100644 index 0000000000..a624c056be --- /dev/null +++ b/resources/quality/malyan_m200/pla/malyan_m200_pla_verydraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Low Detail Draft +definition = malyan_m200 + +[metadata] +type = quality +quality_type = verydraft +material = generic_pla_175 +weight = -4 +setting_version = 4 + +[values] +material_bed_temperature = 60 +material_bed_temperature_layer_0 = 60 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_draft.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_draft.inst.cfg new file mode 100644 index 0000000000..a63256573a --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_draft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fast +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = draft +material = generic_abs_175 +weight = -2 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_fast.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_fast.inst.cfg new file mode 100644 index 0000000000..49f4486596 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_fast.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Normal +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = fast +material = generic_abs_175 +weight = -1 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_high.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_high.inst.cfg new file mode 100644 index 0000000000..eab16a8e2b --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_high.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Finer +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = high +material = generic_abs_175 +weight = 1 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_normal.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_normal.inst.cfg new file mode 100644 index 0000000000..03aeb4067b --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_normal.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = normal +material = generic_abs_175 +weight = 0 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_superdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_superdraft.inst.cfg new file mode 100644 index 0000000000..148f53ba73 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_superdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = superdraft +material = generic_abs_175 +weight = -5 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_thickerdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_thickerdraft.inst.cfg new file mode 100644 index 0000000000..e2ad71a360 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_thickerdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_abs_175 +weight = -3 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_ultra.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_ultra.inst.cfg new file mode 100644 index 0000000000..7ebdf80baf --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_ultra.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Ultra Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_abs_175 +weight = 2 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_verydraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_verydraft.inst.cfg new file mode 100644 index 0000000000..9965ae8bcf --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/abs/monoprice_select_mini_v2_abs_verydraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Low Detail Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = verydraft +material = generic_abs_175 +weight = -4 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Draft_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Draft_Quality.inst.cfg new file mode 100644 index 0000000000..b7d0faa2c7 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Draft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Fast +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = -2 +quality_type = draft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.21875 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Fast_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Fast_Quality.inst.cfg new file mode 100644 index 0000000000..f7f338e4c9 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Fast_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Normal +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = -1 +quality_type = fast +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.175 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_High_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_High_Quality.inst.cfg new file mode 100644 index 0000000000..4a37a1afd8 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_High_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Finer +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = 1 +quality_type = high +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.0875 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Normal_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Normal_Quality.inst.cfg new file mode 100644 index 0000000000..b8e545adcf --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Normal_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = 0 +quality_type = normal +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.13125 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_SuperDraft_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_SuperDraft_Quality.inst.cfg new file mode 100644 index 0000000000..0ef9db5875 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_SuperDraft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = -5 +quality_type = superdraft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.35 +layer_height_0 = 0.35 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_ThickerDraft_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_ThickerDraft_Quality.inst.cfg new file mode 100644 index 0000000000..4dd3a7aafe --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_ThickerDraft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = -3 +quality_type = thickerdraft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.2625 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Ultra_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Ultra_Quality.inst.cfg new file mode 100644 index 0000000000..337f0d06bc --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_Ultra_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Ultra Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = 2 +quality_type = ultra +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.04375 +layer_height_0 = 0.2625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_VeryDraft_Quality.inst.cfg b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_VeryDraft_Quality.inst.cfg new file mode 100644 index 0000000000..e884077069 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/monoprice_select_mini_v2_global_VeryDraft_Quality.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 2 +name = Low Detail Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +weight = -4 +quality_type = verydraft +global_quality = True +setting_version = 4 + +[values] +layer_height = 0.30625 +layer_height_0 = 0.30625 +wall_thickness = 1.05 +top_bottom_thickness = 0.72 +infill_sparse_density = 22 +speed_print = 50 +speed_layer_0 = =round(speed_print * 30 / 50) +speed_topbottom = 20 +cool_min_layer_time = 5 +cool_min_speed = 10 diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_draft.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_draft.inst.cfg new file mode 100644 index 0000000000..4a03c17a63 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_draft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fast +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = draft +material = generic_nylon_175 +weight = -2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_fast.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_fast.inst.cfg new file mode 100644 index 0000000000..1c04f77b8b --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_fast.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Normal +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = fast +material = generic_nylon_175 +weight = -1 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_high.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_high.inst.cfg new file mode 100644 index 0000000000..d57516598a --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_high.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Finer +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = high +material = generic_nylon_175 +weight = 1 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_normal.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_normal.inst.cfg new file mode 100644 index 0000000000..308ea86311 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_normal.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = normal +material = generic_nylon_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_superdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_superdraft.inst.cfg new file mode 100644 index 0000000000..db4f3ca907 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_superdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = superdraft +material = generic_nylon_175 +weight = -5 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_thickerdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_thickerdraft.inst.cfg new file mode 100644 index 0000000000..9a1afc0e48 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_thickerdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_nylon_175 +weight = -3 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_ultra.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_ultra.inst.cfg new file mode 100644 index 0000000000..3453671a72 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_ultra.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Ultra Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = ultra +material = generic_nylon_175 +weight = 2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_verydraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_verydraft.inst.cfg new file mode 100644 index 0000000000..ee2531fc4e --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/nylon/monoprice_select_mini_v2_nylon_verydraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Low Detail Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = verydraft +material = generic_nylon_175 +weight = -4 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_draft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_draft.inst.cfg new file mode 100644 index 0000000000..aa5fc7844d --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_draft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fast +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = draft +material = generic_pc_175 +weight = -2 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_fast.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_fast.inst.cfg new file mode 100644 index 0000000000..232c4ab6f3 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_fast.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Normal +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = fast +material = generic_pc_175 +weight = -1 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_high.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_high.inst.cfg new file mode 100644 index 0000000000..aa9da322fb --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_high.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Finer +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = high +material = generic_pc_175 +weight = 1 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_normal.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_normal.inst.cfg new file mode 100644 index 0000000000..145b21221b --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_normal.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = normal +material = generic_pc_175 +weight = 0 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_superdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_superdraft.inst.cfg new file mode 100644 index 0000000000..b6e53bda62 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_superdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = superdraft +material = generic_pc_175 +weight = -5 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_thickerdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_thickerdraft.inst.cfg new file mode 100644 index 0000000000..055228ab13 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_thickerdraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_pc_175 +weight = -3 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_ultra.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_ultra.inst.cfg new file mode 100644 index 0000000000..a3e99b998e --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_ultra.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Ultra Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = ultra +material = generic_pc_175 +weight = 2 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_verydraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_verydraft.inst.cfg new file mode 100644 index 0000000000..73f5a2f2c9 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pc/monoprice_select_mini_v2_pc_verydraft.inst.cfg @@ -0,0 +1,15 @@ +[general] +version = 2 +name = Low Detail Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = verydraft +material = generic_pc_175 +weight = -4 +setting_version = 4 + +[values] +material_bed_temperature = 70 +material_bed_temperature_layer_0 = 70 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_draft.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_draft.inst.cfg new file mode 100644 index 0000000000..8a33e03310 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_draft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fast +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = draft +material = generic_petg_175 +weight = -2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_fast.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_fast.inst.cfg new file mode 100644 index 0000000000..fb084fa08e --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_fast.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Normal +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = fast +material = generic_petg_175 +weight = -1 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_high.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_high.inst.cfg new file mode 100644 index 0000000000..16891f6f43 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_high.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Finer +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = high +material = generic_petg_175 +weight = 1 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_normal.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_normal.inst.cfg new file mode 100644 index 0000000000..bb2f0b47a8 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_normal.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = normal +material = generic_petg_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_superdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_superdraft.inst.cfg new file mode 100644 index 0000000000..78ca1b6b7a --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_superdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = superdraft +material = generic_petg_175 +weight = -5 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_thickerdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_thickerdraft.inst.cfg new file mode 100644 index 0000000000..69606ff913 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_thickerdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_petg_175 +weight = -3 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_ultra.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_ultra.inst.cfg new file mode 100644 index 0000000000..7c5ac599c8 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_ultra.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Ultra Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = ultra +material = generic_petg_175 +weight = 2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_verydraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_verydraft.inst.cfg new file mode 100644 index 0000000000..ed0c2510f5 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/petg/monoprice_select_mini_v2_petg_verydraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Low Detail Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = verydraft +material = generic_petg_175 +weight = -4 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_draft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_draft.inst.cfg new file mode 100644 index 0000000000..04a955cf6c --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_draft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fast +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = draft +material = generic_pla_175 +weight = -2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_fast.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_fast.inst.cfg new file mode 100644 index 0000000000..6efc0935e2 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_fast.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Normal +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = fast +material = generic_pla_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_high.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_high.inst.cfg new file mode 100644 index 0000000000..8fe2371e5d --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_high.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Finer +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = high +material = generic_pla_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_normal.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_normal.inst.cfg new file mode 100644 index 0000000000..01351154c4 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_normal.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = normal +material = generic_pla_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_superdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_superdraft.inst.cfg new file mode 100644 index 0000000000..adfced9787 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_superdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Lowest Quality Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = superdraft +material = generic_pla_175 +weight = -5 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_thickerdraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_thickerdraft.inst.cfg new file mode 100644 index 0000000000..f4522c9778 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_thickerdraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = thickerdraft +material = generic_pla_175 +weight = -3 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_ultra.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_ultra.inst.cfg new file mode 100644 index 0000000000..2fa8eb7f81 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_ultra.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Ultra Fine +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = ultra +material = generic_pla_175 +weight = 2 +setting_version = 4 \ No newline at end of file diff --git a/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_verydraft.inst.cfg b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_verydraft.inst.cfg new file mode 100644 index 0000000000..e59cf4a490 --- /dev/null +++ b/resources/quality/monoprice_select_mini_v2/pla/monoprice_select_mini_v2_pla_verydraft.inst.cfg @@ -0,0 +1,11 @@ +[general] +version = 2 +name = Low Detail Draft +definition = monoprice_select_mini_v2 + +[metadata] +type = quality +quality_type = verydraft +material = generic_pla_175 +weight = 0 +setting_version = 4 \ No newline at end of file diff --git a/resources/variants/malyan_m200_0.15mm.inst.cfg b/resources/variants/malyan_m200_0.15mm.inst.cfg deleted file mode 100644 index 808b8755ea..0000000000 --- a/resources/variants/malyan_m200_0.15mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.15 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.15 -machine_nozzle_tip_outer_diameter = 0.8 -coasting_volume = 0.05 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.2, 1) -speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) -speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.25mm.inst.cfg b/resources/variants/malyan_m200_0.25mm.inst.cfg deleted file mode 100644 index 3847f5c617..0000000000 --- a/resources/variants/malyan_m200_0.25mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.25 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.25 -machine_nozzle_tip_outer_diameter = 0.8 -coasting_volume = 0.1 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.2, 1) -speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) -speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.30mm.inst.cfg b/resources/variants/malyan_m200_0.30mm.inst.cfg deleted file mode 100644 index fee8aae40f..0000000000 --- a/resources/variants/malyan_m200_0.30mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.30 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.30 -machine_nozzle_tip_outer_diameter = 0.8 -coasting_volume = 0.11 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.2, 1) -speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) -speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.35mm.inst.cfg b/resources/variants/malyan_m200_0.35mm.inst.cfg deleted file mode 100644 index dcbb3ed4c0..0000000000 --- a/resources/variants/malyan_m200_0.35mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.35 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.35 -machine_nozzle_tip_outer_diameter = 0.8 -coasting_volume = 0.13 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.2, 1) -speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) -speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.40mm.inst.cfg b/resources/variants/malyan_m200_0.40mm.inst.cfg deleted file mode 100644 index 6e17dd13a6..0000000000 --- a/resources/variants/malyan_m200_0.40mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.40 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.40 -machine_nozzle_tip_outer_diameter = 1.05 -coasting_volume = 0.15 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.25, 1) -speed_wall_0 = =1 if speed_wall < 10 else (speed_wall - 10) -speed_topbottom = =round(speed_print / 2.25, 1) diff --git a/resources/variants/malyan_m200_0.50mm.inst.cfg b/resources/variants/malyan_m200_0.50mm.inst.cfg deleted file mode 100644 index ce87def1e6..0000000000 --- a/resources/variants/malyan_m200_0.50mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.50 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.50 -machine_nozzle_tip_outer_diameter = 0.8 -coasting_volume = 0.2 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.2, 1) -speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) -speed_topbottom = =round(speed_print / 1.5, 1) diff --git a/resources/variants/malyan_m200_0.60mm.inst.cfg b/resources/variants/malyan_m200_0.60mm.inst.cfg deleted file mode 100644 index 0ee7c786e8..0000000000 --- a/resources/variants/malyan_m200_0.60mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.60 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.60 -machine_nozzle_tip_outer_diameter = 1.25 -coasting_volume = 0.28 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print * 4 / 3, 1) -speed_wall_0 = =1 if speed_wall < 10 else (speed_wall - 10) -speed_topbottom = =round(speed_print / 2, 1) diff --git a/resources/variants/malyan_m200_0.80mm.inst.cfg b/resources/variants/malyan_m200_0.80mm.inst.cfg deleted file mode 100644 index 54f48afdb4..0000000000 --- a/resources/variants/malyan_m200_0.80mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 0.80 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 0.80 -machine_nozzle_tip_outer_diameter = 1.35 -coasting_volume = 0.45 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print * 4 / 3, 1) -speed_wall_0 = =1 if speed_wall < 10 else (speed_wall - 10) -speed_topbottom = =round(speed_print / 2, 1) diff --git a/resources/variants/malyan_m200_1.00mm.inst.cfg b/resources/variants/malyan_m200_1.00mm.inst.cfg deleted file mode 100644 index ce0a2d0dfb..0000000000 --- a/resources/variants/malyan_m200_1.00mm.inst.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[general] -name = 1.00 mm -version = 2 -definition = malyan_m200 - -[metadata] -author = Brian Corbino -type = variant -setting_version = 3 - -[values] -machine_nozzle_size = 1.00 -machine_nozzle_tip_outer_diameter = 0.8 -coasting_volume = 0.63 -coasting_min_volume = =round(coasting_volume * 4,2) -speed_wall = =round(speed_print / 1.2, 1) -speed_wall_0 = =1 if speed_wall < 5 else (speed_wall - 5) -speed_topbottom = =round(speed_print / 1.5, 1) From 2ca06f383e9d65a2b396f395427756ecbf8fafef Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 8 Jan 2018 09:41:18 +0100 Subject: [PATCH 144/200] 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 733d6234e6d821ca552c70106c47d027db4ed6ec Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 11:13:16 +0100 Subject: [PATCH 145/200] CURA-4525 moved experimental preferences to bottom with its own label; all settings are by default off --- cura/CuraApplication.py | 4 +- resources/qml/Preferences/GeneralPage.qml | 71 ++++++++++++++--------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8aeeb9c1e8..3013f6e116 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -319,7 +319,7 @@ class CuraApplication(QtApplication): preferences.addPreference("cura/asked_dialog_on_project_save", False) preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask") - preferences.addPreference("cura/arrange_objects_on_load", True) + preferences.addPreference("cura/not_arrange_objects_on_load", False) preferences.addPreference("cura/use_multi_build_plate", False) preferences.addPreference("cura/currency", "€") @@ -1428,7 +1428,7 @@ class CuraApplication(QtApplication): self.fileLoaded.emit(filename) arrange_objects_on_load = ( not Preferences.getInstance().getValue("cura/use_multi_build_plate") or - Preferences.getInstance().getValue("cura/arrange_objects_on_load")) + not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load")) target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 for original_node in nodes: diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index e5ed9e46c5..ac5cacdbf6 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -453,34 +453,6 @@ UM.PreferencesPage text: catalog.i18nc("@label","Opening and saving files") } - UM.TooltipArea { - width: childrenRect.width - height: childrenRect.height - text: catalog.i18nc("@info:tooltip","Use multi build plate functionality (EXPERIMENTAL)") - - CheckBox - { - id: useMultiBuildPlateCheckbox - text: catalog.i18nc("@option:check","Use multi build plate functionality (EXPERIMENTAL, restart)") - checked: boolCheck(UM.Preferences.getValue("cura/use_multi_build_plate")) - onCheckedChanged: UM.Preferences.setValue("cura/use_multi_build_plate", checked) - } - } - - UM.TooltipArea { - width: childrenRect.width - height: childrenRect.height - text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)") - - CheckBox - { - id: arrangeOnLoadCheckbox - text: catalog.i18nc("@option:check","Arrange objects on load (EXPERIMENTAL)") - checked: boolCheck(UM.Preferences.getValue("cura/arrange_objects_on_load")) - onCheckedChanged: UM.Preferences.setValue("cura/arrange_objects_on_load", checked) - } - } - UM.TooltipArea { width: childrenRect.width height: childrenRect.height @@ -688,6 +660,49 @@ UM.PreferencesPage onCheckedChanged: UM.Preferences.setValue("info/send_slice_info", checked) } } + + Item + { + //: Spacer + height: UM.Theme.getSize("default_margin").height + width: UM.Theme.getSize("default_margin").height + } + + Label + { + font.bold: true + text: catalog.i18nc("@label","Experimental") + } + + UM.TooltipArea { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Use multi build plate functionality") + + CheckBox + { + id: useMultiBuildPlateCheckbox + text: catalog.i18nc("@option:check","Use multi build plate functionality (restart required)") + checked: boolCheck(UM.Preferences.getValue("cura/use_multi_build_plate")) + onCheckedChanged: UM.Preferences.setValue("cura/use_multi_build_plate", checked) + } + } + + UM.TooltipArea { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)") + + CheckBox + { + id: arrangeOnLoadCheckbox + text: catalog.i18nc("@option:check","Do not arrange objects on load") + checked: boolCheck(UM.Preferences.getValue("cura/not_arrange_objects_on_load")) + onCheckedChanged: UM.Preferences.setValue("cura/not_arrange_objects_on_load", checked) + } + } + + } } } From e638c21acbaf53a79aaf742ede3a6feacfd9c059 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 11:30:57 +0100 Subject: [PATCH 146/200] CURA-4525 fix selected object will select correct build plate (not only visualization) --- cura/Scene/CuraSceneController.py | 6 +++--- resources/qml/ObjectsList.qml | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 65723db52c..7931e13659 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -75,11 +75,11 @@ class CuraSceneController(QObject): # Single select item = self._objects_model.getItem(index) node = item["node"] - Selection.clear() - Selection.add(node) build_plate_number = node.callDecoration("getBuildPlateNumber") if build_plate_number is not None and build_plate_number != -1: - self._build_plate_model.setActiveBuildPlate(build_plate_number) + self.setActiveBuildPlate(build_plate_number) + Selection.clear() + Selection.add(node) self._last_selected_index = index diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index a02ea2288d..87f76774da 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -105,7 +105,6 @@ Rectangle topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; - //bottom: objectsList.top; bottomMargin: UM.Theme.getSize("default_margin").height; } From df1bf419d93a909fcceeb052ed2daaf668b3d728 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 8 Jan 2018 11:44:30 +0100 Subject: [PATCH 147/200] 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 52b75b58e791ea5cdc5ff019daffd09fdc126ce5 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 13:25:39 +0100 Subject: [PATCH 148/200] CURA-4525 allow for empty build plates between filled build plates --- plugins/CuraEngineBackend/CuraEngineBackend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 0ca500ecec..28ca793855 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -215,8 +215,9 @@ class CuraEngineBackend(QObject, Backend): num_objects = self._numObjects() if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: self._scene.gcode_list[build_plate_to_be_sliced] = [] - Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced) - return + Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) + if self._build_plates_to_be_sliced: + self._invokeSlice() self._stored_layer_data = [] self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] From 86eb9c73bad0d20b1e04ba984fa25dde21ad7a6e Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 13:40:46 +0100 Subject: [PATCH 149/200] CURA-4525 do not send empty build plates gcode; direct calling slice when skipping empty build plate --- plugins/CuraEngineBackend/CuraEngineBackend.py | 2 +- .../UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 28ca793855..249b2f6cbb 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -217,7 +217,7 @@ class CuraEngineBackend(QObject, Backend): self._scene.gcode_list[build_plate_to_be_sliced] = [] Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: - self._invokeSlice() + return self.slice() self._stored_layer_data = [] self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py index 05069d1c0d..6eb89d69a4 100644 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -282,6 +282,9 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job output_build_plate_number = self._job_list.pop(0) gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[output_build_plate_number] + if not gcode: # Empty build plate + Logger.log("d", "Skipping empty job (build plate number %d).", output_build_plate_number) + return self.sendPrintJob() self._send_gcode_start = time.time() Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number)) From 99de75a3fdafd5b1d653b638364dba2d61041712 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 8 Jan 2018 13:44:20 +0100 Subject: [PATCH 150/200] 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 164e5fdc8e4afe102fd8c41a82e858d9fd9323e1 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 13:55:42 +0100 Subject: [PATCH 151/200] CURA-4525 change active build plate if that build plate does not exist anymore; disable context menu items for build plates when no selection --- cura/Scene/CuraSceneController.py | 7 +++++++ resources/qml/Menus/ContextMenu.qml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 7931e13659..e594752f1e 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -41,6 +41,13 @@ class CuraSceneController(QObject): self._build_plate_model.setMaxBuildPlate(self._max_build_plate) build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)] self._build_plate_model.setItems(build_plates) + if self._active_build_plate > self._max_build_plate: + build_plate_number = 0 + if self._last_selected_index >= 0: # go to the buildplate of the item you last selected + item = self._objects_model.getItem(self._last_selected_index) + node = item["node"] + build_plate_number = node.callDecoration("getBuildPlateNumber") + self.setActiveBuildPlate(build_plate_number) # self.buildPlateItemsChanged.emit() # TODO: necessary after setItems? def _calcMaxBuildPlate(self): diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 1a4b421572..b5f51f4d63 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -47,6 +47,7 @@ Menu { model: Cura.BuildPlateModel MenuItem { + enabled: UM.Selection.hasSelection text: Cura.BuildPlateModel.getItem(index).name; onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.getItem(index).buildPlateNumber); checkable: true @@ -58,6 +59,7 @@ Menu } MenuItem { + enabled: UM.Selection.hasSelection text: "New build plate"; onTriggered: { CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.maxBuildPlate + 1); From c1cb86a334b1c6a7d70fa033664073fead69152d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 14:00:06 +0100 Subject: [PATCH 152/200] CURA-4525 remove initial 'Unable to slice' --- plugins/CuraEngineBackend/CuraEngineBackend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 249b2f6cbb..ba68687c1e 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -217,7 +217,8 @@ class CuraEngineBackend(QObject, Backend): self._scene.gcode_list[build_plate_to_be_sliced] = [] Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: - return self.slice() + self.slice() + return self._stored_layer_data = [] self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] From 584f477b54d08df58d0a8f40ea619d3d4f886e11 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 14:28:09 +0100 Subject: [PATCH 153/200] CURA-4525 fix deleting last item --- cura/Scene/CuraSceneController.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index e594752f1e..c3e27ca3dd 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -45,8 +45,9 @@ class CuraSceneController(QObject): build_plate_number = 0 if self._last_selected_index >= 0: # go to the buildplate of the item you last selected item = self._objects_model.getItem(self._last_selected_index) - node = item["node"] - build_plate_number = node.callDecoration("getBuildPlateNumber") + if "node" in item: + node = item["node"] + build_plate_number = node.callDecoration("getBuildPlateNumber") self.setActiveBuildPlate(build_plate_number) # self.buildPlateItemsChanged.emit() # TODO: necessary after setItems? From d05f4a493b57042b5a9dd08c11a768994b364d92 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 8 Jan 2018 14:41:39 +0100 Subject: [PATCH 154/200] CURA-4525 fix qml undefined QString --- resources/qml/ObjectsList.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 87f76774da..489e38e8d7 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -138,7 +138,7 @@ Rectangle anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 - text: Cura.ObjectsModel.getItem(index) ? Cura.ObjectsModel.getItem(index).name : ""; + text: (index >= 0) && Cura.ObjectsModel.getItem(index) ? Cura.ObjectsModel.getItem(index).name : ""; color: Cura.ObjectsModel.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectsModel.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) elide: Text.ElideRight } From c8cef9583e9801b1a4a60c2095654f80df9a0b47 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 8 Jan 2018 16:18:41 +0100 Subject: [PATCH 155/200] rename scene.gcode_list to gcode_dict CURA-4741 With the multi build plate feature, scene.gcode_list is now a dict which stores a list of gcode for a build plate, so it makes more sense to have it renamed to "gcode_dict" because it's not a list. --- .../CuraEngineBackend/CuraEngineBackend.py | 16 ++++++------- plugins/CuraEngineBackend/ProcessGCodeJob.py | 4 +++- plugins/GCodeReader/FlavorParser.py | 5 +++- plugins/GCodeWriter/GCodeWriter.py | 5 +++- .../NetworkClusterPrinterOutputDevice.py | 23 +++++++++++-------- .../NetworkPrinterOutputDevice.py | 2 +- plugins/USBPrinting/USBPrinterOutputDevice.py | 5 +++- 7 files changed, 37 insertions(+), 23 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index ba68687c1e..c612e889a4 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -205,8 +205,8 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", " ## Process layers job still busy, trying later") return - if not hasattr(self._scene, "gcode_list"): - self._scene.gcode_list = {} + if not hasattr(self._scene, "gcode_dict"): + self._scene.gcode_dict = {} # see if we really have to slice active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate @@ -214,7 +214,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjects() if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: - self._scene.gcode_list[build_plate_to_be_sliced] = [] + self._scene.gcode_dict[build_plate_to_be_sliced] = [] Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: self.slice() @@ -234,7 +234,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - self._scene.gcode_list[build_plate_to_be_sliced] = [] #[] indexed by build plate number + self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number self._slicing = True self.slicingStarted.emit() @@ -393,7 +393,7 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.Disabled) gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: - self._scene.gcode_list[node.callDecoration("getBuildPlateNumber")] = gcode_list + self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list if self._use_timer == enable_timer: return self._use_timer @@ -560,7 +560,7 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.Done) self.processingProgress.emit(1.0) - gcode_list = self._scene.gcode_list[self._start_slice_job_build_plate] + gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] for index, line in enumerate(gcode_list): replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths)) @@ -590,14 +590,14 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message): - self._scene.gcode_list[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) + self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) ## Called when a g-code prefix message is received from the engine. # # \param message The protobuf message containing the g-code prefix, # encoded as UTF-8. def _onGCodePrefixMessage(self, message): - self._scene.gcode_list[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) + self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) ## Creates a new socket connection. def _createSocket(self): diff --git a/plugins/CuraEngineBackend/ProcessGCodeJob.py b/plugins/CuraEngineBackend/ProcessGCodeJob.py index 4974907c30..ed430f8fa9 100644 --- a/plugins/CuraEngineBackend/ProcessGCodeJob.py +++ b/plugins/CuraEngineBackend/ProcessGCodeJob.py @@ -12,4 +12,6 @@ class ProcessGCodeLayerJob(Job): self._message = message def run(self): - self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace")) + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_list = self._scene.gcode_dict[active_build_plate_id] + gcode_list.append(self._message.data.decode("utf-8", "replace")) diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index fa5d6da243..a05c4e1d63 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -430,7 +430,10 @@ class FlavorParser: gcode_list_decorator.setGCodeList(gcode_list) scene_node.addDecorator(gcode_list_decorator) - Application.getInstance().getController().getScene().gcode_list = gcode_list + # gcode_dict stores gcode_lists for a number of build plates. + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_dict = {active_build_plate_id: gcode_list} + Application.getInstance().getController().getScene().gcode_dict = gcode_dict Logger.log("d", "Finished parsing %s" % file_name) self._message.hide() diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index ad23f2c8ee..f0e5c88f37 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -61,7 +61,10 @@ class GCodeWriter(MeshWriter): active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate scene = Application.getInstance().getController().getScene() - gcode_list = getattr(scene, "gcode_list")[active_build_plate] + gcode_dict = getattr(scene, "gcode_dict") + if not gcode_dict: + return False + gcode_list = gcode_dict.get(active_build_plate) if gcode_list: for gcode in gcode_list: stream.write(gcode) diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py index 6eb89d69a4..6665380f45 100644 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -244,8 +244,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] # the build plates to be sent - gcodes = getattr(Application.getInstance().getController().getScene(), "gcode_list") - self._job_list = list(gcodes.keys()) + gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict") + self._job_list = list(gcode_dict.keys()) Logger.log("d", "build plates to be sent to printer: %s", (self._job_list)) if self._stage != OutputStage.ready: @@ -281,11 +281,14 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte def sendPrintJob(self): nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job output_build_plate_number = self._job_list.pop(0) - gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[output_build_plate_number] - if not gcode: # Empty build plate + gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")[output_build_plate_number] + if not gcode_dict: # Empty build plate Logger.log("d", "Skipping empty job (build plate number %d).", output_build_plate_number) return self.sendPrintJob() + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_list = gcode_dict[active_build_plate_id] + self._send_gcode_start = time.time() Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number)) @@ -302,7 +305,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte require_printer_name = self._selected_printer["unique_name"] - new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode) + new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode_list) if new_request is None or self._stage != OutputStage.uploading: return self._request = new_request @@ -310,7 +313,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._reply.uploadProgress.connect(self._onUploadProgress) # See _finishedPrintJobPostRequest() - def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode): + def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode_list): api_url = QUrl(self._api_base_uri + "print_jobs/") request = QNetworkRequest(api_url) # Create multipart request and add the g-code. @@ -321,7 +324,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file"; filename="%s"' % (self._file_name)) - compressed_gcode = self._compressGcode(gcode) + compressed_gcode = self._compressGcode(gcode_list) if compressed_gcode is None: return None # User aborted print, so stop trying. @@ -339,7 +342,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._addUserAgentHeader(request) return request - def _compressGcode(self, gcode): + def _compressGcode(self, gcode_list): self._compressing_print = True batched_line = "" max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB @@ -354,11 +357,11 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._last_response_time = time.time() return compressed_data - if gcode is None: + if gcode_list is None: Logger.log("e", "Unable to find sliced gcode, returning empty.") return byte_array_file_data - for line in gcode: + for line in gcode_list: if not self._compressing_print: self._progress_message.hide() return None # Stop trying to zip, abort was called. diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py index de0a06527e..6b8946b755 100755 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py @@ -676,7 +676,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): self._print_finished = True self.writeStarted.emit(self) active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate - self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[active_build_plate] + self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_dict")[active_build_plate] print_information = Application.getInstance().getPrintInformation() warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about. diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 1930f5402b..f4f5478216 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -163,7 +163,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def startPrint(self): self.writeStarted.emit(self) - gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list") + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict") + gcode_list = gcode_dict[active_build_plate_id] + self._updateJobState("printing") self.printGCode(gcode_list) From 4c8c4c78da77cb3eb86fe078e4cba5eb62c122fb Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 9 Jan 2018 09:16:21 +0100 Subject: [PATCH 156/200] CURA-4772 Create a CuraSceneNode instead of a SceneNode when reading a GCode. Avoid creating an empty CuraSceneNode if the node is already an instance of it. --- cura/CuraApplication.py | 4 +++- plugins/GCodeReader/FlavorParser.py | 8 ++++---- plugins/SimulationView/SimulationPass.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8aeeb9c1e8..b28e17f792 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1432,7 +1432,9 @@ class CuraApplication(QtApplication): target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 for original_node in nodes: - node = CuraSceneNode() # We want our own CuraSceneNode + + # Create a CuraSceneNode just if the original node is not that type + node = original_node if isinstance(original_node, CuraSceneNode) else CuraSceneNode() node.setMeshData(original_node.getMeshData()) node.setSelectable(True) diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index fa5d6da243..0cd49e2fca 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -8,14 +8,14 @@ from UM.Logger import Logger from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.Vector import Vector from UM.Message import Message -from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode from UM.i18n import i18nCatalog from UM.Preferences import Preferences catalog = i18nCatalog("cura") from cura import LayerDataBuilder -from cura import LayerDataDecorator +from cura.LayerDataDecorator import LayerDataDecorator from cura.LayerPolygon import LayerPolygon from cura.Scene.GCodeListDecorator import GCodeListDecorator from cura.Settings.ExtruderManager import ExtruderManager @@ -292,7 +292,7 @@ class FlavorParser: # We obtain the filament diameter from the selected printer to calculate line widths self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") - scene_node = SceneNode() + scene_node = CuraSceneNode() # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no # real data to calculate it from. scene_node.getBoundingBox = self._getNullBoundingBox @@ -422,7 +422,7 @@ class FlavorParser: material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) - decorator = LayerDataDecorator.LayerDataDecorator() + decorator = LayerDataDecorator() decorator.setLayerData(layer_mesh) scene_node.addDecorator(decorator) diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index b453020ffa..c9c1443bfe 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -106,7 +106,7 @@ class SimulationPass(RenderPass): nozzle_node = node nozzle_node.setVisible(False) - elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible() and node.callDecoration("getBuildPlateNumber") == active_build_plate: + elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible(): layer_data = node.callDecoration("getLayerData") if not layer_data: continue From 117c467829c27833a8e0fbc555114aa243e4f468 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Tue, 9 Jan 2018 09:39:15 +0100 Subject: [PATCH 157/200] Fix: Per mesh setting combobox did not save selected option CURA-4760 --- .../PerObjectSettingVisibilityHandler.py | 9 +++++++++ plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml | 2 ++ 2 files changed, 11 insertions(+) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py index badca13468..c7e4deaaa7 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal +from UM.FlameProfiler import pyqtSlot from UM.Application import Application from UM.Settings.ContainerRegistry import ContainerRegistry @@ -21,6 +22,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand self._selected_object_id = None self._node = None self._stack = None + self._skip_setting = None def setSelectedObjectId(self, id): if id != self._selected_object_id: @@ -36,6 +38,10 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand def selectedObjectId(self): return self._selected_object_id + @pyqtSlot(str) + def setSkipSetting(self, setting_name): + self._skip_setting = setting_name + def setVisible(self, visible): if not self._node: return @@ -50,6 +56,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand # Remove all instances that are not in visibility list for instance in all_instances: + # exceptionally skip setting + if self._skip_setting is not None and self._skip_setting == instance.definition.key: + continue if instance.definition.key not in visible: settings.removeInstance(instance.definition.key) visibility_changed = True diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 5bdb6d4cb0..2b7b6cd541 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -324,6 +324,8 @@ Item { // force updating the model to sync it with addedSettingsModel if(visible) { + // Set skip setting, it will prevent from restting selected mesh_type + contents.model.visibilityHandler.setSkipSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) listview.model.forceUpdate() } } From a85fd0c996bb49392a7595d63225130a73651f5b Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 10:26:13 +0100 Subject: [PATCH 158/200] Stop ignoring post processing plugin --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 71e83433cf..a91d3f9377 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ plugins/Doodle3D-cura-plugin plugins/FlatProfileExporter plugins/GodMode plugins/OctoPrintPlugin -plugins/PostProcessingPlugin plugins/ProfileFlattener plugins/X3GWriter From 1f2007554de56741b516373362ec77d02ef9caee Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 10:27:28 +0100 Subject: [PATCH 159/200] Add post processing source files to main repo --- plugins/PostProcessingPlugin | 1 + 1 file changed, 1 insertion(+) create mode 160000 plugins/PostProcessingPlugin diff --git a/plugins/PostProcessingPlugin b/plugins/PostProcessingPlugin new file mode 160000 index 0000000000..476c04484f --- /dev/null +++ b/plugins/PostProcessingPlugin @@ -0,0 +1 @@ +Subproject commit 476c04484f998b4defe42e62d8925d66a626973e From 9c9c46aade61b26af21fb50b2a7c3d80d856f085 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 10:30:02 +0100 Subject: [PATCH 160/200] Remove submodule --- plugins/PostProcessingPlugin | 1 - 1 file changed, 1 deletion(-) delete mode 160000 plugins/PostProcessingPlugin diff --git a/plugins/PostProcessingPlugin b/plugins/PostProcessingPlugin deleted file mode 160000 index 476c04484f..0000000000 --- a/plugins/PostProcessingPlugin +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 476c04484f998b4defe42e62d8925d66a626973e From 6c0fb110fe842758c7cc20dae7edc055d5c8ad8c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 10:31:12 +0100 Subject: [PATCH 161/200] Add post processing source files --- .../PostProcessingPlugin.py | 206 +++++++ .../PostProcessingPlugin.qml | 501 ++++++++++++++++++ plugins/PostProcessingPlugin/README.md | 2 + plugins/PostProcessingPlugin/Script.py | 111 ++++ plugins/PostProcessingPlugin/__init__.py | 11 + plugins/PostProcessingPlugin/plugin.json | 8 + .../PostProcessingPlugin/postprocessing.svg | 47 ++ .../scripts/BQ_PauseAtHeight.py | 48 ++ .../scripts/ColorChange.py | 76 +++ .../scripts/ExampleScript.py | 43 ++ .../scripts/PauseAtHeight.py | 221 ++++++++ .../scripts/PauseAtHeightforRepetier.py | 169 ++++++ .../scripts/SearchAndReplace.py | 56 ++ .../PostProcessingPlugin/scripts/Stretch.py | 469 ++++++++++++++++ .../PostProcessingPlugin/scripts/TweakAtZ.py | 495 +++++++++++++++++ 15 files changed, 2463 insertions(+) create mode 100644 plugins/PostProcessingPlugin/PostProcessingPlugin.py create mode 100644 plugins/PostProcessingPlugin/PostProcessingPlugin.qml create mode 100644 plugins/PostProcessingPlugin/README.md create mode 100644 plugins/PostProcessingPlugin/Script.py create mode 100644 plugins/PostProcessingPlugin/__init__.py create mode 100644 plugins/PostProcessingPlugin/plugin.json create mode 100644 plugins/PostProcessingPlugin/postprocessing.svg create mode 100644 plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py create mode 100644 plugins/PostProcessingPlugin/scripts/ColorChange.py create mode 100644 plugins/PostProcessingPlugin/scripts/ExampleScript.py create mode 100644 plugins/PostProcessingPlugin/scripts/PauseAtHeight.py create mode 100644 plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py create mode 100644 plugins/PostProcessingPlugin/scripts/SearchAndReplace.py create mode 100644 plugins/PostProcessingPlugin/scripts/Stretch.py create mode 100644 plugins/PostProcessingPlugin/scripts/TweakAtZ.py diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py new file mode 100644 index 0000000000..657e5c5387 --- /dev/null +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -0,0 +1,206 @@ +# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot + +from UM.PluginRegistry import PluginRegistry +from UM.Resources import Resources +from UM.Application import Application +from UM.Extension import Extension +from UM.Logger import Logger + +import os.path +import pkgutil +import sys +import importlib.util + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + + +## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated +# g-code files. +class PostProcessingPlugin(QObject, Extension): + def __init__(self, parent = None): + super().__init__(parent) + self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) + self._view = None + + # Loaded scripts are all scripts that can be used + self._loaded_scripts = {} + self._script_labels = {} + + # Script list contains instances of scripts in loaded_scripts. + # There can be duplicates, which will be executed in sequence. + self._script_list = [] + self._selected_script_index = -1 + + Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) + + selectedIndexChanged = pyqtSignal() + @pyqtProperty("QVariant", notify = selectedIndexChanged) + def selectedScriptDefinitionId(self): + try: + return self._script_list[self._selected_script_index].getDefinitionId() + except: + return "" + + @pyqtProperty("QVariant", notify=selectedIndexChanged) + def selectedScriptStackId(self): + try: + return self._script_list[self._selected_script_index].getStackId() + except: + return "" + + ## Execute all post-processing scripts on the gcode. + def execute(self, output_device): + scene = Application.getInstance().getController().getScene() + gcode_dict = getattr(scene, "gcode_dict") + if not gcode_dict: + return + + # get gcode list for the active build plate + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_list = gcode_dict[active_build_plate_id] + if not gcode_list: + return + + if ";POSTPROCESSED" not in gcode_list[0]: + for script in self._script_list: + try: + gcode_list = script.execute(gcode_list) + except Exception: + Logger.logException("e", "Exception in post-processing script.") + if len(self._script_list): # Add comment to g-code if any changes were made. + gcode_list[0] += ";POSTPROCESSED\n" + gcode_dict[active_build_plate_id] = gcode_list + setattr(scene, "gcode_dict", gcode_dict) + else: + Logger.log("e", "Already post processed") + + @pyqtSlot(int) + def setSelectedScriptIndex(self, index): + self._selected_script_index = index + self.selectedIndexChanged.emit() + + @pyqtProperty(int, notify = selectedIndexChanged) + def selectedScriptIndex(self): + return self._selected_script_index + + @pyqtSlot(int, int) + def moveScript(self, index, new_index): + if new_index < 0 or new_index > len(self._script_list) - 1: + return # nothing needs to be done + else: + # Magical switch code. + self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index] + self.scriptListChanged.emit() + self.selectedIndexChanged.emit() #Ensure that settings are updated + self._propertyChanged() + + ## Remove a script from the active script list by index. + @pyqtSlot(int) + def removeScriptByIndex(self, index): + self._script_list.pop(index) + if len(self._script_list) - 1 < self._selected_script_index: + self._selected_script_index = len(self._script_list) - 1 + self.scriptListChanged.emit() + self.selectedIndexChanged.emit() # Ensure that settings are updated + self._propertyChanged() + + ## Load all scripts from provided path. + # This should probably only be done on init. + # \param path Path to check for scripts. + def loadAllScripts(self, path): + scripts = pkgutil.iter_modules(path = [path]) + for loader, script_name, ispkg in scripts: + # Iterate over all scripts. + if script_name not in sys.modules: + spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) + loaded_script = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded_script) + sys.modules[script_name] = loaded_script + + loaded_class = getattr(loaded_script, script_name) + temp_object = loaded_class() + Logger.log("d", "Begin loading of script: %s", script_name) + try: + setting_data = temp_object.getSettingData() + if "name" in setting_data and "key" in setting_data: + self._script_labels[setting_data["key"]] = setting_data["name"] + self._loaded_scripts[setting_data["key"]] = loaded_class + else: + Logger.log("w", "Script %s.py has no name or key", script_name) + self._script_labels[script_name] = script_name + self._loaded_scripts[script_name] = loaded_class + except AttributeError: + Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name) + except NotImplementedError: + Logger.log("e", "Script %s.py has no implemented settings", script_name) + self.loadedScriptListChanged.emit() + + loadedScriptListChanged = pyqtSignal() + @pyqtProperty("QVariantList", notify = loadedScriptListChanged) + def loadedScriptList(self): + return sorted(list(self._loaded_scripts.keys())) + + @pyqtSlot(str, result = str) + def getScriptLabelByKey(self, key): + return self._script_labels[key] + + scriptListChanged = pyqtSignal() + @pyqtProperty("QVariantList", notify = scriptListChanged) + def scriptList(self): + script_list = [script.getSettingData()["key"] for script in self._script_list] + return script_list + + @pyqtSlot(str) + def addScriptToList(self, key): + Logger.log("d", "Adding script %s to list.", key) + new_script = self._loaded_scripts[key]() + self._script_list.append(new_script) + self.setSelectedScriptIndex(len(self._script_list) - 1) + self.scriptListChanged.emit() + self._propertyChanged() + + ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. + def _createView(self): + Logger.log("d", "Creating post processing plugin view.") + + ## Load all scripts in the scripts folders + for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]: + try: + path = os.path.join(root, "scripts") + if not os.path.isdir(path): + try: + os.makedirs(path) + except OSError: + Logger.log("w", "Unable to create a folder for scripts: " + path) + continue + + self.loadAllScripts(path) + except Exception as e: + Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e))) + + # Create the plugin dialog component + path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") + self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) + Logger.log("d", "Post processing view created.") + + # Create the save button component + Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) + + ## Show the (GUI) popup of the post processing plugin. + def showPopup(self): + if self._view is None: + self._createView() + self._view.show() + + ## Property changed: trigger re-slice + # To do this we use the global container stack propertyChanged. + # Re-slicing is necessary for setting changes in this plugin, because the changes + # are applied only once per "fresh" gcode + def _propertyChanged(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack.propertyChanged.emit("post_processing_plugin", "value") + + diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml new file mode 100644 index 0000000000..d64d60a04a --- /dev/null +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml @@ -0,0 +1,501 @@ +// Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +UM.Dialog +{ + id: dialog + + title: catalog.i18nc("@title:window", "Post Processing Plugin") + width: 700 * screenScaleFactor; + height: 500 * screenScaleFactor; + minimumWidth: 400 * screenScaleFactor; + minimumHeight: 250 * screenScaleFactor; + + Item + { + UM.I18nCatalog{id: catalog; name:"cura"} + id: base + property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width) + property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) + property string activeScriptName + SystemPalette{ id: palette } + SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled } + anchors.fill: parent + + ExclusiveGroup + { + id: selectedScriptGroup + } + Item + { + id: activeScripts + anchors.left: parent.left + width: base.columnWidth + height: parent.height + + Label + { + id: activeScriptsHeader + text: catalog.i18nc("@label", "Post Processing Scripts") + anchors.top: parent.top + anchors.topMargin: base.textMargin + anchors.left: parent.left + anchors.leftMargin: base.textMargin + anchors.right: parent.right + anchors.rightMargin: base.textMargin + font: UM.Theme.getFont("large") + } + ListView + { + id: activeScriptsList + anchors.top: activeScriptsHeader.bottom + anchors.topMargin: base.textMargin + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + anchors.rightMargin: base.textMargin + height: childrenRect.height + model: manager.scriptList + delegate: Item + { + width: parent.width + height: activeScriptButton.height + Button + { + id: activeScriptButton + text: manager.getScriptLabelByKey(modelData.toString()) + exclusiveGroup: selectedScriptGroup + checkable: true + checked: { + if (manager.selectedScriptIndex == index) + { + base.activeScriptName = manager.getScriptLabelByKey(modelData.toString()) + return true + } + else + { + return false + } + } + onClicked: + { + forceActiveFocus() + manager.setSelectedScriptIndex(index) + base.activeScriptName = manager.getScriptLabelByKey(modelData.toString()) + } + width: parent.width + height: UM.Theme.getSize("setting").height + style: ButtonStyle + { + background: Rectangle + { + color: activeScriptButton.checked ? palette.highlight : "transparent" + width: parent.width + height: parent.height + } + label: Label + { + wrapMode: Text.Wrap + text: control.text + color: activeScriptButton.checked ? palette.highlightedText : palette.text + } + } + } + Button + { + id: removeButton + text: "x" + width: 20 * screenScaleFactor + height: 20 * screenScaleFactor + anchors.right:parent.right + anchors.rightMargin: base.textMargin + anchors.verticalCenter: parent.verticalCenter + onClicked: manager.removeScriptByIndex(index) + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2.7) + height: Math.floor(control.height / 2.7) + sourceSize.width: width + sourceSize.height: width + color: palette.text + source: UM.Theme.getIcon("cross1") + } + } + } + } + Button + { + id: downButton + text: "" + anchors.right: removeButton.left + anchors.verticalCenter: parent.verticalCenter + enabled: index != manager.scriptList.length - 1 + width: 20 * screenScaleFactor + height: 20 * screenScaleFactor + onClicked: + { + if (manager.selectedScriptIndex == index) + { + manager.setSelectedScriptIndex(index + 1) + } + return manager.moveScript(index, index + 1) + } + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2.5) + height: Math.floor(control.height / 2.5) + sourceSize.width: width + sourceSize.height: width + color: control.enabled ? palette.text : disabledPalette.text + source: UM.Theme.getIcon("arrow_bottom") + } + } + } + } + Button + { + id: upButton + text: "" + enabled: index != 0 + width: 20 * screenScaleFactor + height: 20 * screenScaleFactor + anchors.right: downButton.left + anchors.verticalCenter: parent.verticalCenter + onClicked: + { + if (manager.selectedScriptIndex == index) + { + manager.setSelectedScriptIndex(index - 1) + } + return manager.moveScript(index, index - 1) + } + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2.5) + height: Math.floor(control.height / 2.5) + sourceSize.width: width + sourceSize.height: width + color: control.enabled ? palette.text : disabledPalette.text + source: UM.Theme.getIcon("arrow_top") + } + } + } + } + } + } + Button + { + id: addButton + text: catalog.i18nc("@action", "Add a script") + anchors.left: parent.left + anchors.leftMargin: base.textMargin + anchors.top: activeScriptsList.bottom + anchors.topMargin: base.textMargin + menu: scriptsMenu + style: ButtonStyle + { + label: Label + { + text: control.text + } + } + } + Menu + { + id: scriptsMenu + + Instantiator + { + model: manager.loadedScriptList + + MenuItem + { + text: manager.getScriptLabelByKey(modelData.toString()) + onTriggered: manager.addScriptToList(modelData.toString()) + } + + onObjectAdded: scriptsMenu.insertItem(index, object); + onObjectRemoved: scriptsMenu.removeItem(object); + } + } + } + + Rectangle + { + color: UM.Theme.getColor("sidebar") + anchors.left: activeScripts.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + height: parent.height + id: settingsPanel + + Label + { + id: scriptSpecsHeader + text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName + anchors.top: parent.top + anchors.topMargin: base.textMargin + anchors.left: parent.left + anchors.leftMargin: base.textMargin + anchors.right: parent.right + anchors.rightMargin: base.textMargin + height: 20 * screenScaleFactor + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + + ScrollView + { + id: scrollView + anchors.top: scriptSpecsHeader.bottom + anchors.topMargin: settingsPanel.textMargin + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + visible: manager.selectedScriptDefinitionId != "" + style: UM.Theme.styles.scrollview; + + ListView + { + id: listview + spacing: UM.Theme.getSize("default_lining").height + model: UM.SettingDefinitionsModel + { + id: definitionsModel; + containerId: manager.selectedScriptDefinitionId + showAll: true + } + delegate:Loader + { + id: settingLoader + + width: parent.width + height: + { + if(provider.properties.enabled == "True") + { + if(model.type != undefined) + { + return UM.Theme.getSize("section").height; + } + else + { + return 0; + } + } + else + { + return 0; + } + + } + Behavior on height { NumberAnimation { duration: 100 } } + opacity: provider.properties.enabled == "True" ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + enabled: opacity > 0 + property var definition: model + property var settingDefinitionsModel: definitionsModel + property var propertyProvider: provider + property var globalPropertyProvider: inheritStackProvider + + //Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989 + //In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes, + //causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely. + asynchronous: model.type != "enum" && model.type != "extruder" + + onLoaded: { + settingLoader.item.showRevertButton = false + settingLoader.item.showInheritButton = false + settingLoader.item.showLinkedSettingIcon = false + settingLoader.item.doDepthIndentation = true + settingLoader.item.doQualityUserSettingEmphasis = false + } + + sourceComponent: + { + switch(model.type) + { + case "int": + return settingTextField + case "float": + return settingTextField + case "enum": + return settingComboBox + case "extruder": + return settingExtruder + case "bool": + return settingCheckBox + case "str": + return settingTextField + case "category": + return settingCategory + default: + return settingUnknown + } + } + + UM.SettingPropertyProvider + { + id: provider + containerStackId: manager.selectedScriptStackId + key: model.key ? model.key : "None" + watchedProperties: [ "value", "enabled", "state", "validationState" ] + storeIndex: 0 + } + + // Specialty provider that only watches global_inherits (we cant filter on what property changed we get events + // so we bypass that to make a dedicated provider). + UM.SettingPropertyProvider + { + id: inheritStackProvider + containerStackId: Cura.MachineManager.activeMachineId + key: model.key ? model.key : "None" + watchedProperties: [ "limit_to_extruder" ] + } + + Connections + { + target: item + + onShowTooltip: + { + tooltip.text = text; + var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0); + tooltip.show(position); + tooltip.target.x = position.x + 1 + } + + onHideTooltip: + { + tooltip.hide(); + } + } + + } + } + } + } + + Cura.SidebarTooltip + { + id: tooltip + } + + Component + { + id: settingTextField; + + Cura.SettingTextField { } + } + + Component + { + id: settingComboBox; + + Cura.SettingComboBox { } + } + + Component + { + id: settingExtruder; + + Cura.SettingExtruder { } + } + + Component + { + id: settingCheckBox; + + Cura.SettingCheckBox { } + } + + Component + { + id: settingCategory; + + Cura.SettingCategory { } + } + + Component + { + id: settingUnknown; + + Cura.SettingUnknown { } + } + } + rightButtons: Button + { + text: catalog.i18nc("@action:button", "Close") + iconName: "dialog-close" + onClicked: dialog.accept() + } + + Button { + objectName: "postProcessingSaveAreaButton" + visible: activeScriptsList.count > 0 + height: UM.Theme.getSize("save_button_save_to_button").height + width: height + tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts") + onClicked: dialog.show() + + style: ButtonStyle { + background: Rectangle { + id: deviceSelectionIcon + border.width: UM.Theme.getSize("default_lining").width + border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") : + control.pressed ? UM.Theme.getColor("action_button_active_border") : + control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border") + color: !control.enabled ? UM.Theme.getColor("action_button_disabled") : + control.pressed ? UM.Theme.getColor("action_button_active") : + control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") + Behavior on color { ColorAnimation { duration: 50; } } + anchors.left: parent.left + anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2); + width: parent.height + height: parent.height + + UM.RecolorImage { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(parent.width / 2) + height: Math.floor(parent.height / 2) + sourceSize.width: width + sourceSize.height: height + color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") : + control.pressed ? UM.Theme.getColor("action_button_active_text") : + control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text"); + source: "postprocessing.svg" + } + } + label: Label{ } + } + } +} \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/README.md b/plugins/PostProcessingPlugin/README.md new file mode 100644 index 0000000000..988f40007d --- /dev/null +++ b/plugins/PostProcessingPlugin/README.md @@ -0,0 +1,2 @@ +# PostProcessingPlugin +A post processing plugin for Cura diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py new file mode 100644 index 0000000000..7d603ba11f --- /dev/null +++ b/plugins/PostProcessingPlugin/Script.py @@ -0,0 +1,111 @@ +# Copyright (c) 2015 Jaime van Kessel +# Copyright (c) 2017 Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from UM.Logger import Logger +from UM.Signal import Signal, signalemitter +from UM.i18n import i18nCatalog + +# Setting stuff import +from UM.Application import Application +from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.DefinitionContainer import DefinitionContainer +from UM.Settings.ContainerRegistry import ContainerRegistry + +import re +import json +import collections +i18n_catalog = i18nCatalog("cura") + + +## Base class for scripts. All scripts should inherit the script class. +@signalemitter +class Script: + def __init__(self): + super().__init__() + self._settings = None + self._stack = None + + setting_data = self.getSettingData() + self._stack = ContainerStack(stack_id = str(id(self))) + self._stack.setDirty(False) # This stack does not need to be saved. + + + ## Check if the definition of this script already exists. If not, add it to the registry. + if "key" in setting_data: + definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"]) + if definitions: + # Definition was found + self._definition = definitions[0] + else: + self._definition = DefinitionContainer(setting_data["key"]) + self._definition.deserialize(json.dumps(setting_data)) + ContainerRegistry.getInstance().addContainer(self._definition) + self._stack.addContainer(self._definition) + self._instance = InstanceContainer(container_id="ScriptInstanceContainer") + self._instance.setDefinition(self._definition.getId()) + self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0)) + self._stack.addContainer(self._instance) + self._stack.propertyChanged.connect(self._onPropertyChanged) + + ContainerRegistry.getInstance().addContainer(self._stack) + + settingsLoaded = Signal() + valueChanged = Signal() # Signal emitted whenever a value of a setting is changed + + def _onPropertyChanged(self, key, property_name): + if property_name == "value": + self.valueChanged.emit() + + # Property changed: trigger reslice + # To do this we use the global container stack propertyChanged. + # Reslicing is necessary for setting changes in this plugin, because the changes + # are applied only once per "fresh" gcode + global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack.propertyChanged.emit(key, property_name) + + ## Needs to return a dict that can be used to construct a settingcategory file. + # See the example script for an example. + # It follows the same style / guides as the Uranium settings. + # Scripts can either override getSettingData directly, or use getSettingDataString + # to return a string that will be parsed as json. The latter has the benefit over + # returning a dict in that the order of settings is maintained. + def getSettingData(self): + setting_data = self.getSettingDataString() + if type(setting_data) == str: + setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict) + return setting_data + + def getSettingDataString(self): + raise NotImplementedError() + + def getDefinitionId(self): + if self._stack: + return self._stack.getBottom().getId() + + def getStackId(self): + if self._stack: + return self._stack.getId() + + ## Convenience function that retrieves value of a setting from the stack. + def getSettingValueByKey(self, key): + return self._stack.getProperty(key, "value") + + ## Convenience function that finds the value in a line of g-code. + # When requesting key = x from line "G1 X100" the value 100 is returned. + def getValue(self, line, key, default = None): + if not key in line or (';' in line and line.find(key) > line.find(';')): + return default + sub_part = line[line.find(key) + 1:] + m = re.search('^-?[0-9]+\.?[0-9]*', sub_part) + if m is None: + return default + try: + return float(m.group(0)) + except: + return default + + ## This is called when the script is executed. + # It gets a list of g-code strings and needs to return a (modified) list. + def execute(self, data): + raise NotImplementedError() diff --git a/plugins/PostProcessingPlugin/__init__.py b/plugins/PostProcessingPlugin/__init__.py new file mode 100644 index 0000000000..85f1126136 --- /dev/null +++ b/plugins/PostProcessingPlugin/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. + +from . import PostProcessingPlugin +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") +def getMetaData(): + return {} + +def register(app): + return {"extension": PostProcessingPlugin.PostProcessingPlugin()} \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/plugin.json b/plugins/PostProcessingPlugin/plugin.json new file mode 100644 index 0000000000..ebfef8145a --- /dev/null +++ b/plugins/PostProcessingPlugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Post Processing", + "author": "Ultimaker", + "version": "2.2", + "api": 4, + "description": "Extension that allows for user created scripts for post processing", + "catalog": "cura" +} \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/postprocessing.svg b/plugins/PostProcessingPlugin/postprocessing.svg new file mode 100644 index 0000000000..f55face4a9 --- /dev/null +++ b/plugins/PostProcessingPlugin/postprocessing.svg @@ -0,0 +1,47 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py new file mode 100644 index 0000000000..fb59378206 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py @@ -0,0 +1,48 @@ +from ..Script import Script +class BQ_PauseAtHeight(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Pause at height (BQ Printers)", + "key": "BQ_PauseAtHeight", + "metadata":{}, + "version": 2, + "settings": + { + "pause_height": + { + "label": "Pause height", + "description": "At what height should the pause occur", + "unit": "mm", + "type": "float", + "default_value": 5.0 + } + } + }""" + + def execute(self, data): + x = 0. + y = 0. + current_z = 0. + pause_z = self.getSettingValueByKey("pause_height") + for layer in data: + lines = layer.split("\n") + for line in lines: + if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: + current_z = self.getValue(line, 'Z') + if current_z != None: + if current_z >= pause_z: + prepend_gcode = ";TYPE:CUSTOM\n" + prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z + + # Insert Pause gcode + prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n" + + index = data.index(layer) + layer = prepend_gcode + layer + data[index] = layer # Override the data of this layer with the modified data + return data + break + return data diff --git a/plugins/PostProcessingPlugin/scripts/ColorChange.py b/plugins/PostProcessingPlugin/scripts/ColorChange.py new file mode 100644 index 0000000000..8db45f4033 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/ColorChange.py @@ -0,0 +1,76 @@ +# This PostProcessing Plugin script is released +# under the terms of the AGPLv3 or higher + +from ..Script import Script +#from UM.Logger import Logger +# from cura.Settings.ExtruderManager import ExtruderManager + +class ColorChange(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Color Change", + "key": "ColorChange", + "metadata": {}, + "version": 2, + "settings": + { + "layer_number": + { + "label": "Layer", + "description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.", + "unit": "", + "type": "str", + "default_value": "1" + }, + + "initial_retract": + { + "label": "Initial Retraction", + "description": "Initial filament retraction distance", + "unit": "mm", + "type": "float", + "default_value": 300.0 + }, + "later_retract": + { + "label": "Later Retraction Distance", + "description": "Later filament retraction distance for removal", + "unit": "mm", + "type": "float", + "default_value": 30.0 + } + } + }""" + + def execute(self, data: list): + + """data is a list. Each index contains a layer""" + layer_nums = self.getSettingValueByKey("layer_number") + initial_retract = self.getSettingValueByKey("initial_retract") + later_retract = self.getSettingValueByKey("later_retract") + + color_change = "M600" + + if initial_retract is not None and initial_retract > 0.: + color_change = color_change + (" E%.2f" % initial_retract) + + if later_retract is not None and later_retract > 0.: + color_change = color_change + (" L%.2f" % later_retract) + + color_change = color_change + " ; Generated by ColorChange plugin" + + layer_targets = layer_nums.split(',') + if len(layer_targets) > 0: + for layer_num in layer_targets: + layer_num = int( layer_num.strip() ) + if layer_num < len(data): + layer = data[ layer_num - 1 ] + lines = layer.split("\n") + lines.insert(2, color_change ) + final_line = "\n".join( lines ) + data[ layer_num - 1 ] = final_line + + return data diff --git a/plugins/PostProcessingPlugin/scripts/ExampleScript.py b/plugins/PostProcessingPlugin/scripts/ExampleScript.py new file mode 100644 index 0000000000..416a5f5404 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/ExampleScript.py @@ -0,0 +1,43 @@ +# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from ..Script import Script + +class ExampleScript(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Example script", + "key": "ExampleScript", + "metadata": {}, + "version": 2, + "settings": + { + "test": + { + "label": "Test", + "description": "None", + "unit": "mm", + "type": "float", + "default_value": 0.5, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "1" + }, + "derp": + { + "label": "zomg", + "description": "afgasgfgasfgasf", + "unit": "mm", + "type": "float", + "default_value": 0.5, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "1" + } + } + }""" + + def execute(self, data): + return data \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py new file mode 100644 index 0000000000..925a5a7ac5 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -0,0 +1,221 @@ +from ..Script import Script +# from cura.Settings.ExtruderManager import ExtruderManager + +class PauseAtHeight(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Pause at height", + "key": "PauseAtHeight", + "metadata": {}, + "version": 2, + "settings": + { + "pause_height": + { + "label": "Pause Height", + "description": "At what height should the pause occur", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_park_x": + { + "label": "Park Print Head X", + "description": "What X location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 190 + }, + "head_park_y": + { + "label": "Park Print Head Y", + "description": "What Y location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 190 + }, + "retraction_amount": + { + "label": "Retraction", + "description": "How much filament must be retracted at pause.", + "unit": "mm", + "type": "float", + "default_value": 0 + }, + "retraction_speed": + { + "label": "Retraction Speed", + "description": "How fast to retract the filament.", + "unit": "mm/s", + "type": "float", + "default_value": 25 + }, + "extrude_amount": + { + "label": "Extrude Amount", + "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.", + "unit": "mm", + "type": "float", + "default_value": 0 + }, + "extrude_speed": + { + "label": "Extrude Speed", + "description": "How fast to extrude the material after pause.", + "unit": "mm/s", + "type": "float", + "default_value": 3.3333 + }, + "redo_layers": + { + "label": "Redo Layers", + "description": "Redo a number of previous layers after a pause to increases adhesion.", + "unit": "layers", + "type": "int", + "default_value": 0 + }, + "standby_temperature": + { + "label": "Standby Temperature", + "description": "Change the temperature during the pause", + "unit": "°C", + "type": "int", + "default_value": 0 + }, + "resume_temperature": + { + "label": "Resume Temperature", + "description": "Change the temperature after the pause", + "unit": "°C", + "type": "int", + "default_value": 0 + } + } + }""" + + def execute(self, data: list): + + """data is a list. Each index contains a layer""" + + x = 0. + y = 0. + current_z = 0. + pause_height = self.getSettingValueByKey("pause_height") + retraction_amount = self.getSettingValueByKey("retraction_amount") + retraction_speed = self.getSettingValueByKey("retraction_speed") + extrude_amount = self.getSettingValueByKey("extrude_amount") + extrude_speed = self.getSettingValueByKey("extrude_speed") + park_x = self.getSettingValueByKey("head_park_x") + park_y = self.getSettingValueByKey("head_park_y") + layers_started = False + redo_layers = self.getSettingValueByKey("redo_layers") + standby_temperature = self.getSettingValueByKey("standby_temperature") + resume_temperature = self.getSettingValueByKey("resume_temperature") + + # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value") + # with open("out.txt", "w") as f: + # f.write(T) + + # use offset to calculate the current height: = - + layer_0_z = 0. + got_first_g_cmd_on_layer_0 = False + for layer in data: + lines = layer.split("\n") + for line in lines: + if ";LAYER:0" in line: + layers_started = True + continue + + if not layers_started: + continue + + if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: + current_z = self.getValue(line, 'Z') + if not got_first_g_cmd_on_layer_0: + layer_0_z = current_z + got_first_g_cmd_on_layer_0 = True + + x = self.getValue(line, 'X', x) + y = self.getValue(line, 'Y', y) + if current_z is not None: + current_height = current_z - layer_0_z + if current_height >= pause_height: + index = data.index(layer) + prevLayer = data[index - 1] + prevLines = prevLayer.split("\n") + current_e = 0. + for prevLine in reversed(prevLines): + current_e = self.getValue(prevLine, 'E', -1) + if current_e >= 0: + break + + # include a number of previous layers + for i in range(1, redo_layers + 1): + prevLayer = data[index - i] + layer = prevLayer + layer + + prepend_gcode = ";TYPE:CUSTOM\n" + prepend_gcode += ";added code by post processing\n" + prepend_gcode += ";script: PauseAtHeight.py\n" + prepend_gcode += ";current z: %f \n" % current_z + prepend_gcode += ";current height: %f \n" % current_height + + # Retraction + prepend_gcode += "M83\n" + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60) + + # Move the head away + prepend_gcode += "G1 Z%f F300\n" % (current_z + 1) + prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y) + if current_z < 15: + prepend_gcode += "G1 Z15 F300\n" + + # Disable the E steppers + prepend_gcode += "M84 E0\n" + + # Set extruder standby temperature + prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature) + + # Wait till the user continues printing + prepend_gcode += "M0 ;Do the actual pause\n" + + # Set extruder resume temperature + prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature) + + # Push the filament back, + if retraction_amount != 0: + prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60) + + # Optionally extrude material + if extrude_amount != 0: + prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60) + + # and retract again, the properly primes the nozzle + # when changing filament. + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60) + + # Move the head back + prepend_gcode += "G1 Z%f F300\n" % (current_z + 1) + prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y) + if retraction_amount != 0: + prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60) + prepend_gcode += "G1 F9000\n" + prepend_gcode += "M82\n" + + # reset extrude value to pre pause value + prepend_gcode += "G92 E%f\n" % (current_e) + + layer = prepend_gcode + layer + + + # Override the data of this layer with the + # modified data + data[index] = layer + return data + break + return data diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py new file mode 100644 index 0000000000..710baab26a --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py @@ -0,0 +1,169 @@ +from ..Script import Script +class PauseAtHeightforRepetier(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Pause at height for repetier", + "key": "PauseAtHeightforRepetier", + "metadata": {}, + "version": 2, + "settings": + { + "pause_height": + { + "label": "Pause height", + "description": "At what height should the pause occur", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_park_x": + { + "label": "Park print head X", + "description": "What x location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_park_y": + { + "label": "Park print head Y", + "description": "What y location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_move_Z": + { + "label": "Head move Z", + "description": "The Hieght of Z-axis retraction before parking.", + "unit": "mm", + "type": "float", + "default_value": 15.0 + }, + "retraction_amount": + { + "label": "Retraction", + "description": "How much fillament must be retracted at pause.", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "extrude_amount": + { + "label": "Extrude amount", + "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.", + "unit": "mm", + "type": "float", + "default_value": 90.0 + }, + "redo_layers": + { + "label": "Redo layers", + "description": "Redo a number of previous layers after a pause to increases adhesion.", + "unit": "layers", + "type": "int", + "default_value": 0 + } + } + }""" + + def execute(self, data): + x = 0. + y = 0. + current_z = 0. + pause_z = self.getSettingValueByKey("pause_height") + retraction_amount = self.getSettingValueByKey("retraction_amount") + extrude_amount = self.getSettingValueByKey("extrude_amount") + park_x = self.getSettingValueByKey("head_park_x") + park_y = self.getSettingValueByKey("head_park_y") + move_Z = self.getSettingValueByKey("head_move_Z") + layers_started = False + redo_layers = self.getSettingValueByKey("redo_layers") + for layer in data: + lines = layer.split("\n") + for line in lines: + if ";LAYER:0" in line: + layers_started = True + continue + + if not layers_started: + continue + + if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: + current_z = self.getValue(line, 'Z') + x = self.getValue(line, 'X', x) + y = self.getValue(line, 'Y', y) + if current_z != None: + if current_z >= pause_z: + + index = data.index(layer) + prevLayer = data[index-1] + prevLines = prevLayer.split("\n") + current_e = 0. + for prevLine in reversed(prevLines): + current_e = self.getValue(prevLine, 'E', -1) + if current_e >= 0: + break + + prepend_gcode = ";TYPE:CUSTOM\n" + prepend_gcode += ";added code by post processing\n" + prepend_gcode += ";script: PauseAtHeightforRepetier.py\n" + prepend_gcode += ";current z: %f \n" % (current_z) + prepend_gcode += ";current X: %f \n" % (x) + prepend_gcode += ";current Y: %f \n" % (y) + + #Retraction + prepend_gcode += "M83\n" + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount) + + #Move the head away + prepend_gcode += "G1 Z%f F300\n" % (1 + current_z) + prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y) + if current_z < move_Z: + prepend_gcode += "G1 Z%f F300\n" % (current_z + move_Z) + + #Disable the E steppers + prepend_gcode += "M84 E0\n" + #Wait till the user continues printing + prepend_gcode += "@pause now change filament and press continue printing ;Do the actual pause\n" + + #Push the filament back, + if retraction_amount != 0: + prepend_gcode += "G1 E%f F6000\n" % (retraction_amount) + + # Optionally extrude material + if extrude_amount != 0: + prepend_gcode += "G1 E%f F200\n" % (extrude_amount) + prepend_gcode += "@info wait for cleaning nozzle from previous filament\n" + prepend_gcode += "@pause remove the waste filament from parking area and press continue printing\n" + + # and retract again, the properly primes the nozzle when changing filament. + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount) + + #Move the head back + prepend_gcode += "G1 Z%f F300\n" % (1 + current_z) + prepend_gcode +="G1 X%f Y%f F9000\n" % (x, y) + if retraction_amount != 0: + prepend_gcode +="G1 E%f F6000\n" % (retraction_amount) + prepend_gcode +="G1 F9000\n" + prepend_gcode +="M82\n" + + # reset extrude value to pre pause value + prepend_gcode +="G92 E%f\n" % (current_e) + + layer = prepend_gcode + layer + + # include a number of previous layers + for i in range(1, redo_layers + 1): + prevLayer = data[index-i] + layer = prevLayer + layer + + data[index] = layer #Override the data of this layer with the modified data + return data + break + return data diff --git a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py new file mode 100644 index 0000000000..68d697e470 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py @@ -0,0 +1,56 @@ +# Copyright (c) 2017 Ruben Dulek +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. + +import re #To perform the search and replace. + +from ..Script import Script + +## Performs a search-and-replace on all g-code. +# +# Due to technical limitations, the search can't cross the border between +# layers. +class SearchAndReplace(Script): + def getSettingDataString(self): + return """{ + "name": "Search and Replace", + "key": "SearchAndReplace", + "metadata": {}, + "version": 2, + "settings": + { + "search": + { + "label": "Search", + "description": "All occurrences of this text will get replaced by the replacement text.", + "type": "str", + "default_value": "" + }, + "replace": + { + "label": "Replace", + "description": "The search text will get replaced by this text.", + "type": "str", + "default_value": "" + }, + "is_regex": + { + "label": "Use Regular Expressions", + "description": "When enabled, the search text will be interpreted as a regular expression.", + "type": "bool", + "default_value": false + } + } + }""" + + def execute(self, data): + search_string = self.getSettingValueByKey("search") + if not self.getSettingValueByKey("is_regex"): + search_string = re.escape(search_string) #Need to search for the actual string, not as a regex. + search_regex = re.compile(search_string) + + replace_string = self.getSettingValueByKey("replace") + + for layer_number, layer in enumerate(data): + data[layer_number] = re.sub(search_regex, replace_string, layer) #Replace all. + + return data \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/Stretch.py b/plugins/PostProcessingPlugin/scripts/Stretch.py new file mode 100644 index 0000000000..bcb923d3ff --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/Stretch.py @@ -0,0 +1,469 @@ +# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher. +""" +Copyright (c) 2017 Christophe Baribaud 2017 +Python implementation of https://github.com/electrocbd/post_stretch +Correction of hole sizes, cylinder diameters and curves +See the original description in https://github.com/electrocbd/post_stretch + +WARNING This script has never been tested with several extruders +""" +from ..Script import Script +import numpy as np +from UM.Logger import Logger +from UM.Application import Application +import re + +def _getValue(line, key, default=None): + """ + Convenience function that finds the value in a line of g-code. + When requesting key = x from line "G1 X100" the value 100 is returned. + It is a copy of Stript's method, so it is no DontRepeatYourself, but + I split the class into setup part (Stretch) and execution part (Strecher) + and only the setup part inherits from Script + """ + if not key in line or (";" in line and line.find(key) > line.find(";")): + return default + sub_part = line[line.find(key) + 1:] + number = re.search(r"^-?[0-9]+\.?[0-9]*", sub_part) + if number is None: + return default + return float(number.group(0)) + +class GCodeStep(): + """ + Class to store the current value of each G_Code parameter + for any G-Code step + """ + def __init__(self, step): + self.step = step + self.step_x = 0 + self.step_y = 0 + self.step_z = 0 + self.step_e = 0 + self.step_f = 0 + self.comment = "" + + def readStep(self, line): + """ + Reads gcode from line into self + """ + self.step_x = _getValue(line, "X", self.step_x) + self.step_y = _getValue(line, "Y", self.step_y) + self.step_z = _getValue(line, "Z", self.step_z) + self.step_e = _getValue(line, "E", self.step_e) + self.step_f = _getValue(line, "F", self.step_f) + return + + def copyPosFrom(self, step): + """ + Copies positions of step into self + """ + self.step_x = step.step_x + self.step_y = step.step_y + self.step_z = step.step_z + self.step_e = step.step_e + self.step_f = step.step_f + self.comment = step.comment + return + + +# Execution part of the stretch plugin +class Stretcher(): + """ + Execution part of the stretch algorithm + """ + def __init__(self, line_width, wc_stretch, pw_stretch): + self.line_width = line_width + self.wc_stretch = wc_stretch + self.pw_stretch = pw_stretch + if self.pw_stretch > line_width / 4: + self.pw_stretch = line_width / 4 # Limit value of pushwall stretch distance + self.outpos = GCodeStep(0) + self.vd1 = np.empty((0, 2)) # Start points of segments + # of already deposited material for current layer + self.vd2 = np.empty((0, 2)) # End points of segments + # of already deposited material for current layer + self.layer_z = 0 # Z position of the extrusion moves of the current layer + self.layergcode = "" + + def execute(self, data): + """ + Computes the new X and Y coordinates of all g-code steps + """ + Logger.log("d", "Post stretch with line width = " + str(self.line_width) + + "mm wide circle stretch = " + str(self.wc_stretch)+ "mm" + + "and push wall stretch = " + str(self.pw_stretch) + "mm") + retdata = [] + layer_steps = [] + current = GCodeStep(0) + self.layer_z = 0. + current_e = 0. + for layer in data: + lines = layer.rstrip("\n").split("\n") + for line in lines: + current.comment = "" + if line.find(";") >= 0: + current.comment = line[line.find(";"):] + if _getValue(line, "G") == 0: + current.readStep(line) + onestep = GCodeStep(0) + onestep.copyPosFrom(current) + elif _getValue(line, "G") == 1: + current.readStep(line) + onestep = GCodeStep(1) + onestep.copyPosFrom(current) + elif _getValue(line, "G") == 92: + current.readStep(line) + onestep = GCodeStep(-1) + onestep.copyPosFrom(current) + else: + onestep = GCodeStep(-1) + onestep.copyPosFrom(current) + onestep.comment = line + if line.find(";LAYER:") >= 0 and len(layer_steps): + # Previous plugin "forgot" to separate two layers... + Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z) + + " " + str(len(layer_steps)) + " steps") + retdata.append(self.processLayer(layer_steps)) + layer_steps = [] + layer_steps.append(onestep) + # self.layer_z is the z position of the last extrusion move (not travel move) + if current.step_z != self.layer_z and current.step_e != current_e: + self.layer_z = current.step_z + current_e = current.step_e + if len(layer_steps): # Force a new item in the array + Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z) + + " " + str(len(layer_steps)) + " steps") + retdata.append(self.processLayer(layer_steps)) + layer_steps = [] + retdata.append(";Wide circle stretch distance " + str(self.wc_stretch) + "\n") + retdata.append(";Push wall stretch distance " + str(self.pw_stretch) + "\n") + return retdata + + def extrusionBreak(self, layer_steps, i_pos): + """ + Returns true if the command layer_steps[i_pos] breaks the extruded filament + i.e. it is a travel move + """ + if i_pos == 0: + return True # Begining a layer always breaks filament (for simplicity) + step = layer_steps[i_pos] + prev_step = layer_steps[i_pos - 1] + if step.step_e != prev_step.step_e: + return False + delta_x = step.step_x - prev_step.step_x + delta_y = step.step_y - prev_step.step_y + if delta_x * delta_x + delta_y * delta_y < self.line_width * self.line_width / 4: + # This is a very short movement, less than 0.5 * line_width + # It does not break filament, we should stay in the same extrusion sequence + return False + return True # New sequence + + + def processLayer(self, layer_steps): + """ + Computes the new coordinates of g-code steps + for one layer (all the steps at the same Z coordinate) + """ + self.outpos.step_x = -1000 # Force output of X and Y coordinates + self.outpos.step_y = -1000 # at each start of layer + self.layergcode = "" + self.vd1 = np.empty((0, 2)) + self.vd2 = np.empty((0, 2)) + orig_seq = np.empty((0, 2)) + modif_seq = np.empty((0, 2)) + iflush = 0 + for i, step in enumerate(layer_steps): + if step.step == 0 or step.step == 1: + if self.extrusionBreak(layer_steps, i): + # No extrusion since the previous step, so it is a travel move + # Let process steps accumulated into orig_seq, + # which are a sequence of continuous extrusion + modif_seq = np.copy(orig_seq) + if len(orig_seq) >= 2: + self.workOnSequence(orig_seq, modif_seq) + self.generate(layer_steps, iflush, i, modif_seq) + iflush = i + orig_seq = np.empty((0, 2)) + orig_seq = np.concatenate([orig_seq, np.array([[step.step_x, step.step_y]])]) + if len(orig_seq): + modif_seq = np.copy(orig_seq) + if len(orig_seq) >= 2: + self.workOnSequence(orig_seq, modif_seq) + self.generate(layer_steps, iflush, len(layer_steps), modif_seq) + return self.layergcode + + def stepToGcode(self, onestep): + """ + Converts a step into G-Code + For each of the X, Y, Z, E and F parameter, + the parameter is written only if its value changed since the + previous g-code step. + """ + sout = "" + if onestep.step_f != self.outpos.step_f: + self.outpos.step_f = onestep.step_f + sout += " F{:.0f}".format(self.outpos.step_f).rstrip(".") + if onestep.step_x != self.outpos.step_x or onestep.step_y != self.outpos.step_y: + assert onestep.step_x >= -1000 and onestep.step_x < 1000 # If this assertion fails, + # something went really wrong ! + self.outpos.step_x = onestep.step_x + sout += " X{:.3f}".format(self.outpos.step_x).rstrip("0").rstrip(".") + assert onestep.step_y >= -1000 and onestep.step_y < 1000 # If this assertion fails, + # something went really wrong ! + self.outpos.step_y = onestep.step_y + sout += " Y{:.3f}".format(self.outpos.step_y).rstrip("0").rstrip(".") + if onestep.step_z != self.outpos.step_z or onestep.step_z != self.layer_z: + self.outpos.step_z = onestep.step_z + sout += " Z{:.3f}".format(self.outpos.step_z).rstrip("0").rstrip(".") + if onestep.step_e != self.outpos.step_e: + self.outpos.step_e = onestep.step_e + sout += " E{:.5f}".format(self.outpos.step_e).rstrip("0").rstrip(".") + return sout + + def generate(self, layer_steps, ibeg, iend, orig_seq): + """ + Appends g-code lines to the plugin's returned string + starting from step ibeg included and until step iend excluded + """ + ipos = 0 + for i in range(ibeg, iend): + if layer_steps[i].step == 0: + layer_steps[i].step_x = orig_seq[ipos][0] + layer_steps[i].step_y = orig_seq[ipos][1] + sout = "G0" + self.stepToGcode(layer_steps[i]) + self.layergcode = self.layergcode + sout + "\n" + ipos = ipos + 1 + elif layer_steps[i].step == 1: + layer_steps[i].step_x = orig_seq[ipos][0] + layer_steps[i].step_y = orig_seq[ipos][1] + sout = "G1" + self.stepToGcode(layer_steps[i]) + self.layergcode = self.layergcode + sout + "\n" + ipos = ipos + 1 + else: + self.layergcode = self.layergcode + layer_steps[i].comment + "\n" + + + def workOnSequence(self, orig_seq, modif_seq): + """ + Computes new coordinates for a sequence + A sequence is a list of consecutive g-code steps + of continuous material extrusion + """ + d_contact = self.line_width / 2.0 + if (len(orig_seq) > 2 and + ((orig_seq[len(orig_seq) - 1] - orig_seq[0]) ** 2).sum(0) < d_contact * d_contact): + # Starting and ending point of the sequence are nearby + # It is a closed loop + #self.layergcode = self.layergcode + ";wideCircle\n" + self.wideCircle(orig_seq, modif_seq) + else: + #self.layergcode = self.layergcode + ";wideTurn\n" + self.wideTurn(orig_seq, modif_seq) # It is an open curve + if len(orig_seq) > 6: # Don't try push wall on a short sequence + self.pushWall(orig_seq, modif_seq) + if len(orig_seq): + self.vd1 = np.concatenate([self.vd1, np.array(orig_seq[:-1])]) + self.vd2 = np.concatenate([self.vd2, np.array(orig_seq[1:])]) + + def wideCircle(self, orig_seq, modif_seq): + """ + Similar to wideTurn + The first and last point of the sequence are the same, + so it is possible to extend the end of the sequence + with its beginning when seeking for triangles + + It is necessary to find the direction of the curve, knowing three points (a triangle) + If the triangle is not wide enough, there is a huge risk of finding + an incorrect orientation, due to insufficient accuracy. + So, when the consecutive points are too close, the method + use following and preceding points to form a wider triangle around + the current point + dmin_tri is the minimum distance between two consecutive points + of an acceptable triangle + """ + dmin_tri = self.line_width / 2.0 + iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points + ibeg = 0 # Index of first point of the triangle + iend = 0 # Index of the third point of the triangle + for i, step in enumerate(orig_seq): + if i == 0 or i == len(orig_seq) - 1: + # First and last point of the sequence are the same, + # so it is necessary to skip one of these two points + # when creating a triangle containing the first or the last point + iextra = iextra_base + 1 + else: + iextra = iextra_base + # i is the index of the second point of the triangle + # pos_after is the array of positions of the original sequence + # after the current point + pos_after = np.resize(np.roll(orig_seq, -i-1, 0), (iextra, 2)) + # Vector of distances between the current point and each following point + dist_from_point = ((step - pos_after) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + iend = np.argmax(dist_from_point >= dmin_tri * dmin_tri) + # pos_before is the array of positions of the original sequence + # before the current point + pos_before = np.resize(np.roll(orig_seq, -i, 0)[::-1], (iextra, 2)) + # This time, vector of distances between the current point and each preceding point + dist_from_point = ((step - pos_before) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + ibeg = np.argmax(dist_from_point >= dmin_tri * dmin_tri) + # See https://github.com/electrocbd/post_stretch for explanations + # relpos is the relative position of the projection of the second point + # of the triangle on the segment from the first to the third point + # 0 means the position of the first point, 1 means the position of the third, + # intermediate values are positions between + length_base = ((pos_after[iend] - pos_before[ibeg]) ** 2).sum(0) + relpos = ((step - pos_before[ibeg]) + * (pos_after[iend] - pos_before[ibeg])).sum(0) + if np.fabs(relpos) < 1000.0 * np.fabs(length_base): + relpos /= length_base + else: + relpos = 0.5 # To avoid division by zero or precision loss + projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg])) + dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0)) + if dist_from_proj > 0.001: # Move central point only if points are not aligned + modif_seq[i] = (step - (self.wc_stretch / dist_from_proj) + * (projection - step)) + return + + def wideTurn(self, orig_seq, modif_seq): + ''' + We have to select three points in order to form a triangle + These three points should be far enough from each other to have + a reliable estimation of the orientation of the current turn + ''' + dmin_tri = self.line_width / 2.0 + ibeg = 0 + iend = 2 + for i in range(1, len(orig_seq) - 1): + dist_from_point = ((orig_seq[i] - orig_seq[i+1:]) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + iend = i + 1 + np.argmax(dist_from_point >= dmin_tri * dmin_tri) + dist_from_point = ((orig_seq[i] - orig_seq[i-1::-1]) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + ibeg = i - 1 - np.argmax(dist_from_point >= dmin_tri * dmin_tri) + length_base = ((orig_seq[iend] - orig_seq[ibeg]) ** 2).sum(0) + relpos = ((orig_seq[i] - orig_seq[ibeg]) * (orig_seq[iend] - orig_seq[ibeg])).sum(0) + if np.fabs(relpos) < 1000.0 * np.fabs(length_base): + relpos /= length_base + else: + relpos = 0.5 + projection = orig_seq[ibeg] + relpos * (orig_seq[iend] - orig_seq[ibeg]) + dist_from_proj = np.sqrt(((projection - orig_seq[i]) ** 2).sum(0)) + if dist_from_proj > 0.001: + modif_seq[i] = (orig_seq[i] - (self.wc_stretch / dist_from_proj) + * (projection - orig_seq[i])) + return + + def pushWall(self, orig_seq, modif_seq): + """ + The algorithm tests for each segment if material was + already deposited at one or the other side of this segment. + If material was deposited at one side but not both, + the segment is moved into the direction of the deposited material, + to "push the wall" + + Already deposited material is stored as segments. + vd1 is the array of the starting points of the segments + vd2 is the array of the ending points of the segments + For example, segment nr 8 starts at position self.vd1[8] + and ends at position self.vd2[8] + """ + dist_palp = self.line_width # Palpation distance to seek for a wall + mrot = np.array([[0, -1], [1, 0]]) # Rotation matrix for a quarter turn + for i in range(len(orig_seq)): + ibeg = i # Index of the first point of the segment + iend = i + 1 # Index of the last point of the segment + if iend == len(orig_seq): + iend = i - 1 + xperp = np.dot(mrot, orig_seq[iend] - orig_seq[ibeg]) + xperp = xperp / np.sqrt((xperp ** 2).sum(-1)) + testleft = orig_seq[ibeg] + xperp * dist_palp + materialleft = False # Is there already extruded material at the left of the segment + testright = orig_seq[ibeg] - xperp * dist_palp + materialright = False # Is there already extruded material at the right of the segment + if self.vd1.shape[0]: + relpos = np.clip(((testleft - self.vd1) * (self.vd2 - self.vd1)).sum(1) + / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.) + nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1) + # nearpoints is the array of the nearest points of each segment + # from the point testleft + dist = ((testleft - nearpoints) * (testleft - nearpoints)).sum(1) + # dist is the array of the squares of the distances between testleft + # and each segment + if np.amin(dist) <= dist_palp * dist_palp: + materialleft = True + # Now the same computation with the point testright at the other side of the + # current segment + relpos = np.clip(((testright - self.vd1) * (self.vd2 - self.vd1)).sum(1) + / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.) + nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1) + dist = ((testright - nearpoints) * (testright - nearpoints)).sum(1) + if np.amin(dist) <= dist_palp * dist_palp: + materialright = True + if materialleft and not materialright: + modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch + elif not materialleft and materialright: + modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch + if materialleft and materialright: + modif_seq[ibeg] = orig_seq[ibeg] # Surrounded by walls, don't move + +# Setup part of the stretch plugin +class Stretch(Script): + """ + Setup part of the stretch algorithm + The only parameter is the stretch distance + """ + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Post stretch script", + "key": "Stretch", + "metadata": {}, + "version": 2, + "settings": + { + "wc_stretch": + { + "label": "Wide circle stretch distance", + "description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect", + "unit": "mm", + "type": "float", + "default_value": 0.08, + "minimum_value": 0, + "minimum_value_warning": 0, + "maximum_value_warning": 0.2 + }, + "pw_stretch": + { + "label": "Push Wall stretch distance", + "description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect", + "unit": "mm", + "type": "float", + "default_value": 0.08, + "minimum_value": 0, + "minimum_value_warning": 0, + "maximum_value_warning": 0.2 + } + } + }""" + + def execute(self, data): + """ + Entry point of the plugin. + data is the list of original g-code instructions, + the returned string is the list of modified g-code instructions + """ + stretcher = Stretcher( + Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value") + , self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch")) + return stretcher.execute(data) + diff --git a/plugins/PostProcessingPlugin/scripts/TweakAtZ.py b/plugins/PostProcessingPlugin/scripts/TweakAtZ.py new file mode 100644 index 0000000000..7b714f6ee0 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/TweakAtZ.py @@ -0,0 +1,495 @@ +# TweakAtZ script - Change printing parameters at a given height +# This script is the successor of the TweakAtZ plugin for legacy Cura. +# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V. +# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher. +# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms + +#Authors of the TweakAtZ plugin / script: +# Written by Steven Morlock, smorloc@gmail.com +# Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+ +# Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below) +# Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x +# Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug. + +##history / changelog: +##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment) +##V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at, +## extruder three temperature disabled by "#Ex3" +##V3.1.1: Bugfix reset flow rate +##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift +##V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser, +## added speed reset at the end of the print +##V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option, +## extruder three code removed, tweaking print speed, save call of Publisher class, +## uses previous value from other plugins also on UltiGCode +##V4.0.1: Bugfix for doubled G1 commands +##V4.0.2: uses Cura progress bar instead of its own +##V4.0.3: Bugfix for cool head lift (contributed by luisonoff) +##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin +##V4.9.92: Modifications for Cura 15.10 +##V4.9.93: Minor bugfixes (input settings) / documentation +##V4.9.94: Bugfix Combobox-selection; remove logger +##V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x +##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed' +##V5.1: API Changes included for use with Cura 2.2 + +## Uses - +## M220 S - set speed factor override percentage +## M221 S - set flow factor override percentage +## M221 S T<0-#toolheads> - set flow factor override percentage for single extruder +## M104 S T<0-#toolheads> - set extruder to target temperature +## M140 S - set bed target temperature +## M106 S - set fan speed to target speed +## M605/606 to save and recall material settings on the UM2 + +from ..Script import Script +#from UM.Logger import Logger +import re + +class TweakAtZ(Script): + version = "5.1.1" + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"TweakAtZ """ + self.version + """ (Experimental)", + "key":"TweakAtZ", + "metadata": {}, + "version": 2, + "settings": + { + "a_trigger": + { + "label": "Trigger", + "description": "Trigger at height or at layer no.", + "type": "enum", + "options": {"height":"Height","layer_no":"Layer No."}, + "default_value": "height" + }, + "b_targetZ": + { + "label": "Tweak Height", + "description": "Z height to tweak at", + "unit": "mm", + "type": "float", + "default_value": 5.0, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "230", + "enabled": "a_trigger == 'height'" + }, + "b_targetL": + { + "label": "Tweak Layer", + "description": "Layer no. to tweak at", + "unit": "", + "type": "int", + "default_value": 1, + "minimum_value": "-100", + "minimum_value_warning": "-1", + "enabled": "a_trigger == 'layer_no'" + }, + "c_behavior": + { + "label": "Behavior", + "description": "Select behavior: Tweak value and keep it for the rest, Tweak value for single layer only", + "type": "enum", + "options": {"keep_value":"Keep value","single_layer":"Single Layer"}, + "default_value": "keep_value" + }, + "d_twLayers": + { + "label": "No. Layers", + "description": "No. of layers used to tweak", + "unit": "", + "type": "int", + "default_value": 1, + "minimum_value": "1", + "maximum_value_warning": "50", + "enabled": "c_behavior == 'keep_value'" + }, + "e1_Tweak_speed": + { + "label": "Tweak Speed", + "description": "Select if total speed (print and travel) has to be tweaked", + "type": "bool", + "default_value": false + }, + "e2_speed": + { + "label": "Speed", + "description": "New total speed (print and travel)", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "e1_Tweak_speed" + }, + "f1_Tweak_printspeed": + { + "label": "Tweak Print Speed", + "description": "Select if print speed has to be tweaked", + "type": "bool", + "default_value": false + }, + "f2_printspeed": + { + "label": "Print Speed", + "description": "New print speed", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "f1_Tweak_printspeed" + }, + "g1_Tweak_flowrate": + { + "label": "Tweak Flow Rate", + "description": "Select if flow rate has to be tweaked", + "type": "bool", + "default_value": false + }, + "g2_flowrate": + { + "label": "Flow Rate", + "description": "New Flow rate", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g1_Tweak_flowrate" + }, + "g3_Tweak_flowrateOne": + { + "label": "Tweak Flow Rate 1", + "description": "Select if first extruder flow rate has to be tweaked", + "type": "bool", + "default_value": false + }, + "g4_flowrateOne": + { + "label": "Flow Rate One", + "description": "New Flow rate Extruder 1", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g3_Tweak_flowrateOne" + }, + "g5_Tweak_flowrateTwo": + { + "label": "Tweak Flow Rate 2", + "description": "Select if second extruder flow rate has to be tweaked", + "type": "bool", + "default_value": false + }, + "g6_flowrateTwo": + { + "label": "Flow Rate two", + "description": "New Flow rate Extruder 2", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g5_Tweak_flowrateTwo" + }, + "h1_Tweak_bedTemp": + { + "label": "Tweak Bed Temp", + "description": "Select if Bed Temperature has to be tweaked", + "type": "bool", + "default_value": false + }, + "h2_bedTemp": + { + "label": "Bed Temp", + "description": "New Bed Temperature", + "unit": "C", + "type": "float", + "default_value": 60, + "minimum_value": "0", + "minimum_value_warning": "30", + "maximum_value_warning": "120", + "enabled": "h1_Tweak_bedTemp" + }, + "i1_Tweak_extruderOne": + { + "label": "Tweak Extruder 1 Temp", + "description": "Select if First Extruder Temperature has to be tweaked", + "type": "bool", + "default_value": false + }, + "i2_extruderOne": + { + "label": "Extruder 1 Temp", + "description": "New First Extruder Temperature", + "unit": "C", + "type": "float", + "default_value": 190, + "minimum_value": "0", + "minimum_value_warning": "160", + "maximum_value_warning": "250", + "enabled": "i1_Tweak_extruderOne" + }, + "i3_Tweak_extruderTwo": + { + "label": "Tweak Extruder 2 Temp", + "description": "Select if Second Extruder Temperature has to be tweaked", + "type": "bool", + "default_value": false + }, + "i4_extruderTwo": + { + "label": "Extruder 2 Temp", + "description": "New Second Extruder Temperature", + "unit": "C", + "type": "float", + "default_value": 190, + "minimum_value": "0", + "minimum_value_warning": "160", + "maximum_value_warning": "250", + "enabled": "i3_Tweak_extruderTwo" + }, + "j1_Tweak_fanSpeed": + { + "label": "Tweak Fan Speed", + "description": "Select if Fan Speed has to be tweaked", + "type": "bool", + "default_value": false + }, + "j2_fanSpeed": + { + "label": "Fan Speed", + "description": "New Fan Speed (0-255)", + "unit": "PWM", + "type": "int", + "default_value": 255, + "minimum_value": "0", + "minimum_value_warning": "15", + "maximum_value_warning": "255", + "enabled": "j1_Tweak_fanSpeed" + } + } + }""" + + def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature + if not key in line or (";" in line and line.find(key) > line.find(";") and + not ";TweakAtZ" in key and not ";LAYER:" in key): + return default + subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1 + if ";TweakAtZ" in key: + m = re.search("^[0-4]", subPart) + elif ";LAYER:" in key: + m = re.search("^[+-]?[0-9]*", subPart) + else: + #the minus at the beginning allows for negative values, e.g. for delta printers + m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart) + if m == None: + return default + try: + return float(m.group(0)) + except: + return default + + def execute(self, data): + #Check which tweaks should apply + TweakProp = {"speed": self.getSettingValueByKey("e1_Tweak_speed"), + "flowrate": self.getSettingValueByKey("g1_Tweak_flowrate"), + "flowrateOne": self.getSettingValueByKey("g3_Tweak_flowrateOne"), + "flowrateTwo": self.getSettingValueByKey("g5_Tweak_flowrateTwo"), + "bedTemp": self.getSettingValueByKey("h1_Tweak_bedTemp"), + "extruderOne": self.getSettingValueByKey("i1_Tweak_extruderOne"), + "extruderTwo": self.getSettingValueByKey("i3_Tweak_extruderTwo"), + "fanSpeed": self.getSettingValueByKey("j1_Tweak_fanSpeed")} + TweakPrintSpeed = self.getSettingValueByKey("f1_Tweak_printspeed") + TweakStrings = {"speed": "M220 S%f\n", + "flowrate": "M221 S%f\n", + "flowrateOne": "M221 T0 S%f\n", + "flowrateTwo": "M221 T1 S%f\n", + "bedTemp": "M140 S%f\n", + "extruderOne": "M104 S%f T0\n", + "extruderTwo": "M104 S%f T1\n", + "fanSpeed": "M106 S%d\n"} + target_values = {"speed": self.getSettingValueByKey("e2_speed"), + "printspeed": self.getSettingValueByKey("f2_printspeed"), + "flowrate": self.getSettingValueByKey("g2_flowrate"), + "flowrateOne": self.getSettingValueByKey("g4_flowrateOne"), + "flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"), + "bedTemp": self.getSettingValueByKey("h2_bedTemp"), + "extruderOne": self.getSettingValueByKey("i2_extruderOne"), + "extruderTwo": self.getSettingValueByKey("i4_extruderTwo"), + "fanSpeed": self.getSettingValueByKey("j2_fanSpeed")} + old = {"speed": -1, "flowrate": -1, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1, + "extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1} + twLayers = self.getSettingValueByKey("d_twLayers") + if self.getSettingValueByKey("c_behavior") == "single_layer": + behavior = 1 + else: + behavior = 0 + try: + twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1 + except: + twLayers = 1 + pres_ext = 0 + done_layers = 0 + z = 0 + x = None + y = None + layer = -100000 #layer no. may be negative (raft) but never that low + # state 0: deactivated, state 1: activated, state 2: active, but below z, + # state 3: active and partially executed (multi layer), state 4: active and passed z + state = 1 + # IsUM2: Used for reset of values (ok for Marlin/Sprinter), + # has to be set to 1 for UltiGCode (work-around for missing default values) + IsUM2 = False + oldValueUnknown = False + TWinstances = 0 + + if self.getSettingValueByKey("a_trigger") == "layer_no": + targetL_i = int(self.getSettingValueByKey("b_targetL")) + targetZ = 100000 + else: + targetL_i = -100000 + targetZ = self.getSettingValueByKey("b_targetZ") + index = 0 + for active_layer in data: + modified_gcode = "" + lines = active_layer.split("\n") + for line in lines: + if ";Generated with Cura_SteamEngine" in line: + TWinstances += 1 + modified_gcode += ";TweakAtZ instances: %d\n" % TWinstances + if not ("M84" in line or "M25" in line or ("G1" in line and TweakPrintSpeed and (state==3 or state==4)) or + ";TweakAtZ instances:" in line): + modified_gcode += line + "\n" + IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode! + if ";TweakAtZ-state" in line: #checks for state change comment + state = self.getValue(line, ";TweakAtZ-state", state) + if ";TweakAtZ instances:" in line: + try: + tempTWi = int(line[20:]) + except: + tempTWi = TWinstances + TWinstances = tempTWi + if ";Small layer" in line: #checks for begin of Cool Head Lift + old["state"] = state + state = 0 + if ";LAYER:" in line: #new layer no. found + if state == 0: + state = old["state"] + layer = self.getValue(line, ";LAYER:", layer) + if targetL_i > -100000: #target selected by layer no. + if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for tweak on layer 0 + state = 2 + targetZ = z + 0.001 + if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd + pres_ext = self.getValue(line, "T", pres_ext) + if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed + old["bedTemp"] = self.getValue(line, "S", old["bedTemp"]) + if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed + if self.getValue(line, "T", pres_ext) == 0: + old["extruderOne"] = self.getValue(line, "S", old["extruderOne"]) + elif self.getValue(line, "T", pres_ext) == 1: + old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"]) + if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object + old["fanSpeed"] = 0 + if "M106" in line and state < 3: #looking for fan speed + old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"]) + if "M221" in line and state < 3: #looking for flow rate + tmp_extruder = self.getValue(line,"T",None) + if tmp_extruder == None: #check if extruder is specified + old["flowrate"] = self.getValue(line, "S", old["flowrate"]) + elif tmp_extruder == 0: #first extruder + old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"]) + elif tmp_extruder == 1: #second extruder + old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"]) + if ("M84" in line or "M25" in line): + if state>0 and TweakProp["speed"]: #"finish" commands for UM Original and UM2 + modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n" + modified_gcode += "M117 \n" + modified_gcode += line + "\n" + if "G1" in line or "G0" in line: + newZ = self.getValue(line, "Z", z) + x = self.getValue(line, "X", None) + y = self.getValue(line, "Y", None) + e = self.getValue(line, "E", None) + f = self.getValue(line, "F", None) + if 'G1' in line and TweakPrintSpeed and (state==3 or state==4): + # check for pure print movement in target range: + if x != None and y != None and f != None and e != None and newZ==z: + modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"), + self.getValue(line, "Y"), self.getValue(line, "E")) + else: #G1 command but not a print movement + modified_gcode += line + "\n" + # no tweaking on retraction hops which have no x and y coordinate: + if (newZ != z) and (x is not None) and (y is not None): + z = newZ + if z < targetZ and state == 1: + state = 2 + if z >= targetZ and state == 2: + state = 3 + done_layers = 0 + for key in TweakProp: + if TweakProp[key] and old[key]==-1: #old value is not known + oldValueUnknown = True + if oldValueUnknown: #the tweaking has to happen within one layer + twLayers = 1 + if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2) + modified_gcode += "M605 S%d;stores parameters before tweaking\n" % (TWinstances-1) + if behavior == 1: #single layer tweak only and then reset + twLayers = 1 + if TweakPrintSpeed and behavior == 0: + twLayers = done_layers + 1 + if state==3: + if twLayers-done_layers>0: #still layers to go? + if targetL_i > -100000: + modified_gcode += ";TweakAtZ V%s: executed at Layer %d\n" % (self.version,layer) + modified_gcode += "M117 Printing... tw@L%4d\n" % layer + else: + modified_gcode += (";TweakAtZ V%s: executed at %1.2f mm\n" % (self.version,z)) + modified_gcode += "M117 Printing... tw@%5.1f\n" % z + for key in TweakProp: + if TweakProp[key]: + modified_gcode += TweakStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1)) + done_layers += 1 + else: + state = 4 + if behavior == 1: #reset values after one layer + if targetL_i > -100000: + modified_gcode += ";TweakAtZ V%s: reset on Layer %d\n" % (self.version,layer) + else: + modified_gcode += ";TweakAtZ V%s: reset at %1.2f mm\n" % (self.version,z) + if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting + modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1) + else: #executes on RepRap, UM2 with Ultigcode and Cura setting + for key in TweakProp: + if TweakProp[key]: + modified_gcode += TweakStrings[key] % float(old[key]) + # re-activates the plugin if executed by pre-print G-command, resets settings: + if (z < targetZ or layer == 0) and state >= 3: #resets if below tweak level or at level 0 + state = 2 + done_layers = 0 + if targetL_i > -100000: + modified_gcode += ";TweakAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i) + else: + modified_gcode += ";TweakAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ) + if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting + modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1) + else: #executes on RepRap, UM2 with Ultigcode and Cura setting + for key in TweakProp: + if TweakProp[key]: + modified_gcode += TweakStrings[key] % float(old[key]) + data[index] = modified_gcode + index += 1 + return data From e3e6f301f9172c6bf811ac5ee432c277ba08deb2 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 9 Jan 2018 10:42:03 +0100 Subject: [PATCH 162/200] CURA-4778 Add up to 8 different colors when loading a GCode, since we support up to 8 extruders in CFP --- plugins/GCodeReader/FlavorParser.py | 8 +++++++- plugins/SimulationView/SimulationView.py | 2 +- plugins/SimulationView/SimulationView.qml | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index c604ab1b6f..f63ba3ca69 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -418,9 +418,15 @@ class FlavorParser: self._layer_number += 1 current_path.clear() - material_color_map = numpy.zeros((10, 4), dtype = numpy.float32) + material_color_map = numpy.zeros((8, 4), dtype = numpy.float32) material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0] material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0] + material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0] + material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0] + material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0] + material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0] + material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0] + material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0] layer_mesh = self._layer_data_builder.build(material_color_map) decorator = LayerDataDecorator() decorator.setLayerData(layer_mesh) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 7a716d3b2b..f667aff998 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -104,7 +104,7 @@ class SimulationView(View): title = catalog.i18nc("@info:title", "Simulation View")) def _resetSettings(self): - self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed + self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness self._extruder_count = 0 self._extruder_opacity = [1.0, 1.0, 1.0, 1.0] self._show_travel_moves = 0 diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml index 19ae81a6e3..11b985f77c 100644 --- a/plugins/SimulationView/SimulationView.qml +++ b/plugins/SimulationView/SimulationView.qml @@ -176,7 +176,6 @@ Item viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2); viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3); } - } Label From 59d7cc663a45a59c8e03260357a394fc137221cf Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 10:36:00 +0100 Subject: [PATCH 163/200] Fix creating ExtruderStack for single-extrusion machine at runtime Move per-extruder settings from the machine's quality changes container to the extruder's quality changes container. --- cura/Settings/CuraContainerRegistry.py | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index a078240d80..394e83b977 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -515,6 +515,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id) if extruder_quality_changes_container: extruder_quality_changes_container = extruder_quality_changes_container[0] + quality_changes_id = extruder_quality_changes_container.getId() extruder_stack.setQualityChangesById(quality_changes_id) else: @@ -525,10 +526,36 @@ class CuraContainerRegistry(ContainerRegistry): if extruder_quality_changes_container: quality_changes_id = extruder_quality_changes_container.getId() extruder_stack.setQualityChangesById(quality_changes_id) + else: + # if we still cannot find a quality changes container for the extruder, create a new one + container_id = self.uniqueName(extruder_stack.getId() + "_user") + container_name = machine.qualityChanges.getName() + extruder_quality_changes_container = InstanceContainer(container_id) + extruder_quality_changes_container.setName(container_name) + extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes") + extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) + extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId()) + extruder_quality_changes_container.addMetaDataEntry("quality_type", machine.qualityChanges.getMetaDataEntry("quality_type")) + extruder_quality_changes_container.setDefinition(machine.definition.getId()) if not extruder_quality_changes_container: Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", machine.qualityChanges.getName(), extruder_stack.getId()) + else: + # move all per-extruder settings to the extruder's quality changes + for qc_setting_key in machine.qualityChanges.getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine.qualityChanges.getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + extruder_quality_changes_container.addInstance(new_instance) + extruder_quality_changes_container.setDirty(True) + + machine.qualityChanges.removeInstance(qc_setting_key, postpone_emit=True) else: extruder_stack.setQualityChangesById("empty_quality_changes") From 8e5167be762b1e383023ca788dd095eac1f90522 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 13:23:24 +0100 Subject: [PATCH 164/200] Use the same machine def for extruder quality changes containers --- cura/Settings/CuraContainerRegistry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 394e83b977..9a64f1421e 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -536,7 +536,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId()) extruder_quality_changes_container.addMetaDataEntry("quality_type", machine.qualityChanges.getMetaDataEntry("quality_type")) - extruder_quality_changes_container.setDefinition(machine.definition.getId()) + extruder_quality_changes_container.setDefinition(machine.qualityChanges.getDefinition().getId()) if not extruder_quality_changes_container: Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", From c91eb30de9499e9e6cf813da66d5f65d7116f54b Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 13:24:38 +0100 Subject: [PATCH 165/200] Skip loaded instance containers When trying to apply the fix for single extrusion machines by creating a new ExtruderStack, skip the quality changes container that has already been loaded. --- cura/Settings/CuraContainerRegistry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 9a64f1421e..873876c30d 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -589,6 +589,9 @@ class CuraContainerRegistry(ContainerRegistry): if parser["general"]["name"] == name: # load the container container_id = os.path.basename(file_path).replace(".inst.cfg", "") + if self.findInstanceContainers(id = container_id): + # this container is already in the registry, skip it + continue instance_container = InstanceContainer(container_id) with open(file_path, "r") as f: From 78b9a14007d4bd83432b7d7643d2d9a4fbd122e0 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 13:25:53 +0100 Subject: [PATCH 166/200] Fix all possible quality changes containers for single extrusion In the current fix, the quality changes that are applicable to the machine may not have been fixed yet because they are not active at that moment. This commit makes sure that for a single extrusion machine which has just been updated to have an ExtruderStack, all the quality changes containers that are applicable to this machine will be updated as well. --- cura/Settings/CuraContainerRegistry.py | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 873876c30d..564b83df6d 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -561,6 +561,45 @@ class CuraContainerRegistry(ContainerRegistry): self.addContainer(extruder_stack) + # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have + # per-extruder settings in the container for the machine instead of the extruder. + quality_changes_machine_definition_id = machine.qualityChanges.getDefinition().getId() + qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) + qc_groups = {} # map of qc names -> qc containers + for qc in qcs: + qc_name = qc.getName() + if qc_name not in qc_groups: + qc_groups[qc_name] = [] + qc_groups[qc_name].append(qc) + # try to find from the quality changes cura directory too + quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName()) + if quality_changes_container: + qc_groups[qc_name].append(quality_changes_container) + + for qc_name, qc_list in qc_groups.items(): + qc_dict = {"global": None, "extruders": []} + for qc in qc_list: + extruder_def_id = qc.getMetaDataEntry("extruder") + if extruder_def_id is not None: + qc_dict["extruders"].append(qc) + else: + qc_dict["global"] = qc + if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: + # move per-extruder settings + for qc_setting_key in qc_dict["global"].getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + qc_dict["extruders"][0].addInstance(new_instance) + qc_dict["extruders"][0].setDirty(True) + + qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) + # Set next stack at the end extruder_stack.setNextStack(machine) From d633a4c11265394f6aaa67721f3dfd85221907b8 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Tue, 9 Jan 2018 13:38:41 +0100 Subject: [PATCH 167/200] Fix: cura does not profile form G-code CURA-4776 --- cura/Settings/CuraContainerRegistry.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index a078240d80..15168af0a4 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -202,7 +202,6 @@ class CuraContainerRegistry(ContainerRegistry): for plugin_id, meta_data in self._getIOPlugins("profile_reader"): if meta_data["profile_reader"][0]["extension"] != extension: continue - profile_reader = plugin_registry.getPluginObject(plugin_id) try: profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. @@ -215,6 +214,20 @@ class CuraContainerRegistry(ContainerRegistry): name_seed = os.path.splitext(os.path.basename(file_name))[0] new_name = self.uniqueName(name_seed) + # if the loaded profile comes from g-code then the instance cointaners should be + # defined differently + file_extension = os.path.splitext(file_name)[1][1:] + if file_extension == "gcode": + for item in profile_or_list: + item.metaData["name"] = new_name + + if item.getMetaDataEntry("extruder") is None: + temp_defintion = item.getMetaDataEntry("definition") + item.metaData["id"] = temp_defintion + "_" + new_name + elif item.getMetaDataEntry("extruder") is not None: + temp_extruder = item.getMetaDataEntry("extruder") + item.metaData["id"] = temp_extruder + "_" + new_name + # Ensure it is always a list of profiles if type(profile_or_list) is not list: profile_or_list = [profile_or_list] From 5936ebaa80636dc7d2189f47d6426685151cabb6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 13:48:20 +0100 Subject: [PATCH 168/200] Fix another case of scene node checking by subclass, related to CURA-4780 --- cura/OneAtATimeIterator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 44f8d2766a..755680e62f 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -18,7 +18,7 @@ class OneAtATimeIterator(Iterator.Iterator): def _fillStack(self): node_list = [] for node in self._scene_node.getChildren(): - if not type(node) is SceneNode: + if not issubclass(type(node), SceneNode): continue if node.callDecoration("getConvexHull"): From 33f0e8cb6559cd17721883473f112a507ae1a136 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 13:58:04 +0100 Subject: [PATCH 169/200] Use isinstance() to check if a node is a SceneNode CURA-4780 --- cura/OneAtATimeIterator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 755680e62f..5653c8f1fb 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -18,7 +18,7 @@ class OneAtATimeIterator(Iterator.Iterator): def _fillStack(self): node_list = [] for node in self._scene_node.getChildren(): - if not issubclass(type(node), SceneNode): + if not isinstance(node, SceneNode): continue if node.callDecoration("getConvexHull"): From dcf02137adc8476c0adf77795873abc6991e4c90 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 9 Jan 2018 14:53:16 +0100 Subject: [PATCH 170/200] 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 f9286d4bd59a29ec6c33d3345d5c7c18db53027d Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 15:50:32 +0100 Subject: [PATCH 171/200] Fix color swatch not being updated after color selection change - CURA-4774 --- resources/qml/Preferences/MaterialView.qml | 48 +++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index 311150c6b9..c3f36f5125 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -104,14 +104,13 @@ TabView Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") } - Row - { - width: scrollView.columnWidth; - height: parent.rowHeight; + Row { + width: scrollView.columnWidth + height: parent.rowHeight spacing: Math.floor(UM.Theme.getSize("default_margin").width/2) - Rectangle - { + // color indicator square + Rectangle { id: colorSelector color: properties.color_code @@ -121,17 +120,36 @@ TabView anchors.verticalCenter: parent.verticalCenter - MouseArea { anchors.fill: parent; onClicked: colorDialog.open(); enabled: base.editingEnabled } + // open the color selection dialog on click + MouseArea { + anchors.fill: parent + onClicked: colorDialog.open() + enabled: base.editingEnabled + } } - ReadOnlyTextField - { + + // make sure the color stays connected after changing the color + Binding { + target: colorSelector + property: "color" + value: properties.color_code + } + + // pretty color name text field + ReadOnlyTextField { id: colorLabel; text: properties.color_name; readOnly: !base.editingEnabled onEditingFinished: base.setMetaDataEntry("color_name", properties.color_name, text) } - ColorDialog { id: colorDialog; color: properties.color_code; onAccepted: base.setMetaDataEntry("color_code", properties.color_code, color) } + // popup dialog to select a new color + // if successful it sets the properties.color_code value to the new color + ColorDialog { + id: colorDialog + color: properties.color_code + onAccepted: base.setMetaDataEntry("color_code", properties.color_code, color) + } } Item { width: parent.width; height: UM.Theme.getSize("default_margin").height } @@ -401,11 +419,11 @@ TabView } // Tiny convenience function to check if a value really changed before trying to set it. - function setMetaDataEntry(entry_name, old_value, new_value) - { - if(old_value != new_value) - { - Cura.ContainerManager.setContainerMetaDataEntry(base.containerId, entry_name, new_value); + function setMetaDataEntry(entry_name, old_value, new_value) { + if (old_value != new_value) { + Cura.ContainerManager.setContainerMetaDataEntry(base.containerId, entry_name, new_value) + // make sure the UI properties are updated as well since we don't re-fetch the entire model here + properties[entry_name] = new_value } } From 3de4940d6952a4a6c3c0934e5d7836996b7bd31f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 9 Jan 2018 16:19:29 +0100 Subject: [PATCH 172/200] Simplify creating, duplicating materials in preferences, extract logic to python, code-style fixes --- cura/Settings/ContainerManager.py | 16 +++ resources/qml/Preferences/MaterialsPage.qml | 104 ++++++++------------ 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 209e1ec8fd..eefc109cbc 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -816,6 +816,22 @@ class ContainerManager(QObject): ContainerRegistry.getInstance().addContainer(container_to_add) return self._getMaterialContainerIdForActiveMachine(clone_of_original) + ## Create a duplicate of a material or it's original entry + # + # \return \type{str} the id of the newly created container. + @pyqtSlot(str, result = str) + def duplicateOriginalMaterial(self, material_id): + + # check if the given material has a base file (i.e. was shipped by default) + base_file = self.getContainerMetaDataEntry(material_id, "base_file") + + if base_file == "": + # there is no base file, so duplicate by ID + return self.duplicateMaterial(material_id) + else: + # there is a base file, so duplicate the original material + return self.duplicateMaterial(base_file) + ## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue # # \return \type{str} the id of the newly created container. diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 81c1bd711a..228f9c8ea2 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -132,93 +132,73 @@ UM.ManagementPage } buttons: [ - Button - { - text: catalog.i18nc("@action:button", "Activate"); + + // Activate button + Button { + text: catalog.i18nc("@action:button", "Activate") iconName: "list-activate"; enabled: base.currentItem != null && base.currentItem.id != Cura.MachineManager.activeMaterialId && Cura.MachineManager.hasMaterials - onClicked: - { - forceActiveFocus(); + onClicked: { + forceActiveFocus() Cura.MachineManager.setActiveMaterial(base.currentItem.id) currentItem = base.model.getItem(base.objectList.currentIndex) // Refresh the current item. } }, - Button - { + + // Create button + Button { text: catalog.i18nc("@action:button", "Create") iconName: "list-add" - onClicked: - { - forceActiveFocus(); - var material_id = Cura.ContainerManager.createMaterial() - if(material_id == "") - { - return - } - if(Cura.MachineManager.hasMaterials) - { - Cura.MachineManager.setActiveMaterial(material_id) - } - base.objectList.currentIndex = base.getIndexById(material_id); + onClicked: { + forceActiveFocus() + Cura.ContainerManager.createMaterial() } }, - Button - { + + // Duplicate button + Button { text: catalog.i18nc("@action:button", "Duplicate"); iconName: "list-add"; enabled: base.currentItem != null - onClicked: - { - forceActiveFocus(); - var base_file = Cura.ContainerManager.getContainerMetaDataEntry(base.currentItem.id, "base_file") - // We need to copy the base container instead of the specific variant. - var material_id = base_file == "" ? Cura.ContainerManager.duplicateMaterial(base.currentItem.id): Cura.ContainerManager.duplicateMaterial(base_file) - if(material_id == "") - { - return - } - if(Cura.MachineManager.hasMaterials) - { - Cura.MachineManager.setActiveMaterial(material_id) - } - // TODO: this doesn't work because the source is a bit delayed - base.objectList.currentIndex = base.getIndexById(material_id); + onClicked: { + forceActiveFocus() + Cura.ContainerManager.duplicateOriginalMaterial(base.currentItem.id) } }, - Button - { - text: catalog.i18nc("@action:button", "Remove"); - iconName: "list-remove"; + + // Remove button + Button { + text: catalog.i18nc("@action:button", "Remove") + iconName: "list-remove" enabled: base.currentItem != null && !base.currentItem.readOnly && !Cura.ContainerManager.isContainerUsed(base.currentItem.id) - onClicked: - { - forceActiveFocus(); - confirmDialog.open(); + onClicked: { + forceActiveFocus() + confirmDialog.open() } }, - Button - { - text: catalog.i18nc("@action:button", "Import"); - iconName: "document-import"; - onClicked: - { - forceActiveFocus(); - importDialog.open(); + + // Import button + Button { + text: catalog.i18nc("@action:button", "Import") + iconName: "document-import" + onClicked: { + forceActiveFocus() + importDialog.open() } - visible: true; + visible: true }, - Button - { + + // Export button + Button { text: catalog.i18nc("@action:button", "Export") iconName: "document-export" - onClicked: - { - forceActiveFocus(); - exportDialog.open(); + onClicked: { + forceActiveFocus() + exportDialog.open() } enabled: currentItem != null } + ] Item { From 93599de64239644ea8a86434d5f2bbafa2b47873 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 17:02:39 +0100 Subject: [PATCH 173/200] Update current index in material dialog CURA-4774 There is a delay so we update the current index when the model gets changed. --- resources/qml/Preferences/MaterialsPage.qml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 228f9c8ea2..6b041b895a 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -153,6 +153,15 @@ UM.ManagementPage forceActiveFocus() Cura.ContainerManager.createMaterial() } + + Connections + { + target: base.objectList.model + onItemsChanged: + { + base.objectList.currentIndex = base.getIndexById(Cura.MachineManager.activeMaterialId); + } + } }, // Duplicate button From 30870c5ef2187557c53d133e6953d944ae64f098 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 9 Jan 2018 17:34:53 +0100 Subject: [PATCH 174/200] Refactor skip reset setting CURA-4760 --- .../PerObjectSettingVisibilityHandler.py | 10 ++++++---- .../PerObjectSettingsTool/PerObjectSettingsPanel.qml | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py index c7e4deaaa7..3e1df1c7b8 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py @@ -22,7 +22,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand self._selected_object_id = None self._node = None self._stack = None - self._skip_setting = None + + # this is a set of settings that will be skipped if the user chooses to reset. + self._skip_reset_setting_set = set() def setSelectedObjectId(self, id): if id != self._selected_object_id: @@ -39,8 +41,8 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand return self._selected_object_id @pyqtSlot(str) - def setSkipSetting(self, setting_name): - self._skip_setting = setting_name + def addSkipResetSetting(self, setting_name): + self._skip_reset_setting_set.add(setting_name) def setVisible(self, visible): if not self._node: @@ -57,7 +59,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand # Remove all instances that are not in visibility list for instance in all_instances: # exceptionally skip setting - if self._skip_setting is not None and self._skip_setting == instance.definition.key: + if instance.definition.key in self._skip_reset_setting_set: continue if instance.definition.key not in visible: settings.removeInstance(instance.definition.key) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 2b7b6cd541..10760b9039 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -324,8 +324,8 @@ Item { // force updating the model to sync it with addedSettingsModel if(visible) { - // Set skip setting, it will prevent from restting selected mesh_type - contents.model.visibilityHandler.setSkipSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) + // Set skip setting, it will prevent from resetting selected mesh_type + contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) listview.model.forceUpdate() } } From 2ce73a18397b0d0440a85a10d017c5568953d1b3 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 10 Jan 2018 00:07:43 +0100 Subject: [PATCH 175/200] 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 3f341a34c62cd2125afa1385e40b6af25b5dad7a Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 10 Jan 2018 08:15:00 +0100 Subject: [PATCH 176/200] Fix gcode job sending issue in gcode_dict refactoring --- .../NetworkClusterPrinterOutputDevice.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py index 6665380f45..9289981d64 100644 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -80,6 +80,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._print_view = None self._request_job = [] + self._job_list = [] + self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") @@ -281,14 +283,11 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte def sendPrintJob(self): nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job output_build_plate_number = self._job_list.pop(0) - gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")[output_build_plate_number] - if not gcode_dict: # Empty build plate + gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_dict")[output_build_plate_number] + if not gcode_list: # Empty build plate Logger.log("d", "Skipping empty job (build plate number %d).", output_build_plate_number) return self.sendPrintJob() - active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate - gcode_list = gcode_dict[active_build_plate_id] - self._send_gcode_start = time.time() Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number)) From 91cccedf750ad2307e849af596fd778668610b54 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 10 Jan 2018 08:54:13 +0100 Subject: [PATCH 177/200] Fix upgrading quality changes for single extrusion machines --- cura/Settings/CuraContainerRegistry.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index a7119c55b7..6db33931bd 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -576,7 +576,19 @@ class CuraContainerRegistry(ContainerRegistry): # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have # per-extruder settings in the container for the machine instead of the extruder. - quality_changes_machine_definition_id = machine.qualityChanges.getDefinition().getId() + if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"): + quality_changes_machine_definition_id = machine.qualityChanges.getDefinition().getId() + else: + whole_machine_definition = machine.definition + machine_entry = machine.definition.getMetaDataEntry("machine") + if machine_entry is not None: + container_registry = ContainerRegistry.getInstance() + whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] + + quality_changes_machine_definition_id = "fdmprinter" + if whole_machine_definition.getMetaDataEntry("has_machine_quality"): + quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", + whole_machine_definition.getId()) qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) qc_groups = {} # map of qc names -> qc containers for qc in qcs: From 4801ace5464e2accf1db6a4d62323d3a158e271a Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 10 Jan 2018 08:58:51 +0100 Subject: [PATCH 178/200] Fix monoprice select mini v2 definition CURA-4655 --- resources/definitions/monoprice_select_mini_v2.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/monoprice_select_mini_v2.def.json b/resources/definitions/monoprice_select_mini_v2.def.json index a935cb5a73..87014c136b 100644 --- a/resources/definitions/monoprice_select_mini_v2.def.json +++ b/resources/definitions/monoprice_select_mini_v2.def.json @@ -20,6 +20,6 @@ "retraction_combing": { "default_value": "noskin" }, "retraction_amount" : { "default_value": 2.5}, "retraction_speed" : { "default_value": 40}, - "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, + "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" } } } From b295d915f82667646d6c6c954013899cd029c28d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 11:08:32 +0100 Subject: [PATCH 179/200] Fix line support_infill_rate / support_line_distance for support meshes without generating any support. CURA-4777 --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 6eef6b1e9b..22b7992d64 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3633,7 +3633,7 @@ "minimum_value": "0", "maximum_value_warning": "100", "default_value": 15, - "value": "15 if support_enable else 0", + "value": "15 if support_enable else 0 if support_tree_enable else 15", "enabled": "support_enable or support_tree_enable", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, From fd86cc84e2377134a317f7f9221b1f8e86a1ff1f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 10 Jan 2018 11:17:26 +0100 Subject: [PATCH 180/200] check if parent exists before using width and height properties from it --- plugins/MonitorStage/MonitorMainView.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/MonitorStage/MonitorMainView.qml b/plugins/MonitorStage/MonitorMainView.qml index 15b05bed0a..c48f6d0aab 100644 --- a/plugins/MonitorStage/MonitorMainView.qml +++ b/plugins/MonitorStage/MonitorMainView.qml @@ -8,8 +8,9 @@ import Cura 1.0 as Cura Item { - width: parent.width - height: parent.height + // parent could be undefined as this component is not visible at all times + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 // We show a nice overlay on the 3D viewer when the current output device has no monitor view Rectangle From 95361bbeb200bbbffa4296fb53f4a635f98ae89b Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Wed, 10 Jan 2018 11:19:19 +0100 Subject: [PATCH 181/200] Refactoring: profile from Gcode CURA-4776 --- cura/Settings/CuraContainerRegistry.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index a7119c55b7..b882902ed7 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -214,19 +214,7 @@ class CuraContainerRegistry(ContainerRegistry): name_seed = os.path.splitext(os.path.basename(file_name))[0] new_name = self.uniqueName(name_seed) - # if the loaded profile comes from g-code then the instance cointaners should be - # defined differently - file_extension = os.path.splitext(file_name)[1][1:] - if file_extension == "gcode": - for item in profile_or_list: - item.metaData["name"] = new_name - if item.getMetaDataEntry("extruder") is None: - temp_defintion = item.getMetaDataEntry("definition") - item.metaData["id"] = temp_defintion + "_" + new_name - elif item.getMetaDataEntry("extruder") is not None: - temp_extruder = item.getMetaDataEntry("extruder") - item.metaData["id"] = temp_extruder + "_" + new_name # Ensure it is always a list of profiles if type(profile_or_list) is not list: @@ -250,6 +238,16 @@ class CuraContainerRegistry(ContainerRegistry): else: #More extruders in the imported file than in the machine. continue #Delete the additional profiles. + # if the loaded profile comes from g-code then the instance containers should be + # defined differently + if extension == "gcode": + if profile.getMetaDataEntry("extruder") is None: + temp_defintion = profile.getMetaDataEntry("definition") + profile.metaData["id"] = (temp_defintion + "_" + new_name).lower() + elif profile.getMetaDataEntry("extruder") is not None: # be sure that extruder data exist + temp_extruder = profile.getMetaDataEntry("extruder") + profile.metaData["id"] = (temp_extruder + "_" + new_name).lower() + result = self._configureProfile(profile, profile_id, new_name) if result is not None: return {"status": "error", "message": catalog.i18nc( From 80aaad83e115ef4308bc633de08fc778effec214 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 14:12:21 +0100 Subject: [PATCH 182/200] CURA-4525 fix manual slice slicing more than one build plate at a time --- plugins/CuraEngineBackend/CuraEngineBackend.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index c612e889a4..fa5f4dde4d 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -238,6 +238,8 @@ class CuraEngineBackend(QObject, Backend): self._slicing = True self.slicingStarted.emit() + self.determineAutoSlicing() # Switch timer on or off if appropriate + slice_message = self._socket.createMessage("cura.proto.Slice") self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) self._start_slice_job_build_plate = build_plate_to_be_sliced @@ -584,6 +586,7 @@ class CuraEngineBackend(QObject, Backend): # Somehow this results in an Arcus Error # self.slice() # Testing call slice again, allow backend to restart by using the timer + self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode self._invokeSlice() ## Called when a g-code message is received from the engine. From 686ac2292d85dc38c826de0c230f2cb784d94e73 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 14:13:18 +0100 Subject: [PATCH 183/200] CURA-4525 CURA-4772 fix gcode not overlapping thoughout build plates --- plugins/CuraEngineBackend/ProcessSlicedLayersJob.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index be9c3f73f0..c1fc597d80 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -4,7 +4,6 @@ import gc from UM.Job import Job -from UM.Scene.SceneNode import SceneNode from UM.Application import Application from UM.Mesh.MeshData import MeshData from UM.Preferences import Preferences @@ -17,6 +16,7 @@ from UM.Logger import Logger from UM.Math.Vector import Vector from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Settings.ExtruderManager import ExtruderManager from cura import LayerDataBuilder from cura import LayerDataDecorator @@ -81,7 +81,7 @@ class ProcessSlicedLayersJob(Job): Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) - new_node = SceneNode() + new_node = CuraSceneNode() new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. From dab0ebd2084649c2b4348caed7c70b6a7013848e Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 14:25:09 +0100 Subject: [PATCH 184/200] CURA-4525 fix saving empty gcode giving error --- plugins/GCodeWriter/GCodeWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index f0e5c88f37..95c48c4d9e 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -64,8 +64,8 @@ class GCodeWriter(MeshWriter): gcode_dict = getattr(scene, "gcode_dict") if not gcode_dict: return False - gcode_list = gcode_dict.get(active_build_plate) - if gcode_list: + gcode_list = gcode_dict.get(active_build_plate, None) + if gcode_list is not None: for gcode in gcode_list: stream.write(gcode) # Serialise the current container stack and put it at the end of the file. From 0fdb470ec7eb3fa1766326ceb4ef1eb8fd3b298a Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 14:39:26 +0100 Subject: [PATCH 185/200] CURA-4525 only try to slice next build plate if there are actually build plates to slice --- plugins/CuraEngineBackend/CuraEngineBackend.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index fa5f4dde4d..41522b34f2 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -585,9 +585,10 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "See if there is more to slice...") # Somehow this results in an Arcus Error # self.slice() - # Testing call slice again, allow backend to restart by using the timer - self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode - self._invokeSlice() + # Call slice again using the timer, allowing the backend to restart + if self._build_plates_to_be_sliced: + self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode + self._invokeSlice() ## Called when a g-code message is received from the engine. # From 6262003069ebfd2eb56889eaaf1596301e0c8250 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 16:01:26 +0100 Subject: [PATCH 186/200] CURA-4525 reset the print information for build plates that are marked for slicing --- cura/PrintInformation.py | 3 ++- plugins/CuraEngineBackend/CuraEngineBackend.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 60d3c11a49..838628e37c 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -11,6 +11,7 @@ from UM.Preferences import Preferences from UM.Settings.ContainerRegistry import ContainerRegistry from cura.Settings.ExtruderManager import ExtruderManager +from typing import Dict import math import os.path @@ -177,7 +178,7 @@ class PrintInformation(QObject): self._material_amounts = material_amounts self._calculateInformation(build_plate_number) - def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time): + def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]): total_estimated_time = 0 if build_plate_number not in self._print_time_message_values: diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 41522b34f2..61950b4677 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -444,6 +444,7 @@ class CuraEngineBackend(QObject, Backend): return build_plate_changed.add(source_build_plate_number) + self.printDurationMessage.emit(source_build_plate_number, {}, []) build_plate_changed.discard(None) build_plate_changed.discard(-1) # object not on build plate From d91a2e1ca36ba2ef0311cfd31ed7c75058c8d1be Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 10 Jan 2018 16:04:56 +0100 Subject: [PATCH 187/200] CURA-4525 move reset print information to a better place to prevent it sending info of non existent build plates --- plugins/CuraEngineBackend/CuraEngineBackend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 61950b4677..e8c830b901 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -444,7 +444,6 @@ class CuraEngineBackend(QObject, Backend): return build_plate_changed.add(source_build_plate_number) - self.printDurationMessage.emit(source_build_plate_number, {}, []) build_plate_changed.discard(None) build_plate_changed.discard(-1) # object not on build plate @@ -461,6 +460,7 @@ class CuraEngineBackend(QObject, Backend): for build_plate_number in build_plate_changed: if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) + self.printDurationMessage.emit(source_build_plate_number, {}, []) self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) # if not self._use_timer: @@ -523,7 +523,7 @@ class CuraEngineBackend(QObject, Backend): def _onStackErrorCheckFinished(self): self._is_error_check_scheduled = False - if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing: + if not self._slicing and self._build_plates_to_be_sliced: self.needsSlicing() self._onChanged() From 665c0a81f40959e39530b895b892937b97023bd4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 10 Jan 2018 16:29:36 +0100 Subject: [PATCH 188/200] 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() From 78b42afbcb0eb8673544ca7b75f42855178f875d Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 10 Jan 2018 16:40:58 +0100 Subject: [PATCH 189/200] CURA-4776 Add unique id to the profiles so user can import the same profile several times with different names.It also fixes the issue for importing profiles from GCode. --- cura/Settings/CuraContainerRegistry.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index a0e2abd0b6..9202e57285 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -214,8 +214,6 @@ class CuraContainerRegistry(ContainerRegistry): name_seed = os.path.splitext(os.path.basename(file_name))[0] new_name = self.uniqueName(name_seed) - - # Ensure it is always a list of profiles if type(profile_or_list) is not list: profile_or_list = [profile_or_list] @@ -238,16 +236,6 @@ class CuraContainerRegistry(ContainerRegistry): else: #More extruders in the imported file than in the machine. continue #Delete the additional profiles. - # if the loaded profile comes from g-code then the instance containers should be - # defined differently - if extension == "gcode": - if profile.getMetaDataEntry("extruder") is None: - temp_defintion = profile.getMetaDataEntry("definition") - profile.metaData["id"] = (temp_defintion + "_" + new_name).lower() - elif profile.getMetaDataEntry("extruder") is not None: # be sure that extruder data exist - temp_extruder = profile.getMetaDataEntry("extruder") - profile.metaData["id"] = (temp_extruder + "_" + new_name).lower() - result = self._configureProfile(profile, profile_id, new_name) if result is not None: return {"status": "error", "message": catalog.i18nc( @@ -280,6 +268,10 @@ class CuraContainerRegistry(ContainerRegistry): profile._id = new_id profile.setName(new_name) + # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile + # It also solves an issue with importing profiles from G-Codes + profile.setMetaDataEntry("id", new_id) + if "type" in profile.getMetaData(): profile.setMetaDataEntry("type", "quality_changes") else: From 702d7dd3a838fa0bfe0f7aa152d376bc1cb46054 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 11 Jan 2018 08:23:17 +0100 Subject: [PATCH 190/200] Fix model importing with multi build plate CURA-4782 --- cura/CuraApplication.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index dfb78cd3b2..30fd461868 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1479,7 +1479,14 @@ class CuraApplication(QtApplication): # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) - node.addDecorator(BuildPlateDecorator(target_build_plate)) + # This node is deepcopied from some other node which already has a BuildPlateDecorator, but the deepcopy + # of BuildPlateDecorator produces one that's assoicated with build plate -1. So, here we need to check if + # the BuildPlateDecorator exists or not and always set the correct build plate number. + build_plate_decorator = node.getDecorator(BuildPlateDecorator) + if build_plate_decorator is None: + build_plate_decorator = BuildPlateDecorator(target_build_plate) + node.addDecorator(build_plate_decorator) + build_plate_decorator.setBuildPlateNumber(target_build_plate) op = AddSceneNodeOperation(node, scene.getRoot()) op.push() From b9a99d46922ff52214b7012ed8dea50a89d8524b Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 11 Jan 2018 09:33:07 +0100 Subject: [PATCH 191/200] CURA-4525 fix placement on load to only avoid objects on current build plate --- cura/CuraActions.py | 2 +- cura/CuraApplication.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 2474e218e8..f5aace805b 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -73,7 +73,7 @@ class CuraActions(QObject): # \param count The number of times to multiply the selection. @pyqtSlot(int) def multiplySelection(self, count: int) -> None: - job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8) + job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8) job.start() ## Delete all selected objects. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index dfb78cd3b2..a2ce5c322c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1421,16 +1421,20 @@ class CuraApplication(QtApplication): filename = job.getFileName() self._currently_loading_files.remove(filename) - root = self.getController().getScene().getRoot() - arranger = Arrange.create(scene_root = root) - min_offset = 8 - self.fileLoaded.emit(filename) arrange_objects_on_load = ( not Preferences.getInstance().getValue("cura/use_multi_build_plate") or not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load")) target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 + root = self.getController().getScene().getRoot() + fixed_nodes = [] + for node_ in DepthFirstIterator(root): + if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate: + fixed_nodes.append(node_) + arranger = Arrange.create(fixed_nodes = fixed_nodes) + min_offset = 8 + for original_node in nodes: # Create a CuraSceneNode just if the original node is not that type From 3f4a2c565cd76663089dc6422d2124847c210c28 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Thu, 11 Jan 2018 13:32:05 +0100 Subject: [PATCH 192/200] Fix: after extruders switch do not trigger slicing, provided a new signal which is triggered after adding extruder and it subscribers for property changes --- cura/Settings/ExtruderManager.py | 4 ++++ plugins/CuraEngineBackend/CuraEngineBackend.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 351843ae14..b5f9a35914 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -49,6 +49,9 @@ class ExtruderManager(QObject): ## Notify when the user switches the currently active extruder. activeExtruderChanged = pyqtSignal() + ## The signal notifies subscribers if extruders are added + extrudersAdded = pyqtSignal() + ## Gets the unique identifier of the currently active extruder stack. # # The currently active extruder stack is the stack that is currently being @@ -406,6 +409,7 @@ class ExtruderManager(QObject): if extruders_changed: self.extrudersChanged.emit(global_stack_id) + self.extrudersAdded.emit() self.setActiveExtruderIndex(0) ## Get all extruder values for a certain setting. diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e8c830b901..e7db4a12dd 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -88,7 +88,7 @@ class CuraEngineBackend(QObject, Backend): # self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged) + Application.getInstance().getExtruderManager().extrudersAdded.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) From 87105e6d063f198eba466dd522bf146c12bafc5d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 11 Jan 2018 14:30:59 +0100 Subject: [PATCH 193/200] CURA-4525 fix printing dimensions --- cura/CuraApplication.py | 10 ++++++++-- cura/Scene/CuraSceneController.py | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7cc8afae1e..3dd4c34815 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -266,6 +266,7 @@ class CuraApplication(QtApplication): self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity) self.getController().toolOperationStopped.connect(self._onToolOperationStopped) self.getController().contextMenuRequested.connect(self._onContextMenuRequested) + self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity) Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.Firmware, "firmware") @@ -890,12 +891,17 @@ class CuraApplication(QtApplication): def getSceneBoundingBoxString(self): return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()} + ## Update scene bounding box for current build plate def updatePlatformActivity(self, node = None): count = 0 scene_bounding_box = None is_block_slicing_node = False + active_build_plate = self.getBuildPlateModel().activeBuildPlate for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): + if ( + not issubclass(type(node), CuraSceneNode) or + (not node.getMeshData() and not node.callDecoration("getLayerData")) or + (node.callDecoration("getBuildPlateNumber") != active_build_plate)): continue if node.callDecoration("isBlockSlicing"): is_block_slicing_node = True @@ -915,7 +921,7 @@ class CuraApplication(QtApplication): if not scene_bounding_box: scene_bounding_box = AxisAlignedBox.Null - if repr(self._scene_bounding_box) != repr(scene_bounding_box) and scene_bounding_box.isValid(): + if repr(self._scene_bounding_box) != repr(scene_bounding_box): self._scene_bounding_box = scene_bounding_box self.sceneBoundingBoxChanged.emit() diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index c3e27ca3dd..97cffcdd7d 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -10,9 +10,12 @@ from UM.Application import Application from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection +from UM.Signal import Signal class CuraSceneController(QObject): + activeBuildPlateChanged = Signal() + def __init__(self, objects_model: ObjectsModel, build_plate_model: BuildPlateModel): super().__init__() @@ -101,6 +104,7 @@ class CuraSceneController(QObject): self._build_plate_model.setActiveBuildPlate(nr) self._objects_model.setActiveBuildPlate(nr) + self.activeBuildPlateChanged.emit() @staticmethod def createCuraSceneController(): From 225a205d97ddc4de0534b112d7f92f41cd5994c1 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 12 Jan 2018 09:19:03 +0100 Subject: [PATCH 194/200] Fixes for material duplication and editing, small refactoring - CURA-4787 --- resources/qml/Preferences/MaterialView.qml | 52 ++++++++++----------- resources/qml/Preferences/MaterialsPage.qml | 9 ---- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index c3f36f5125..b2307fe4f6 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -72,7 +72,7 @@ TabView width: scrollView.columnWidth; text: properties.name; readOnly: !base.editingEnabled; - onEditingFinished: base.setName(properties.name, text) + onEditingFinished: base.updateMaterialDisplayName(properties.name, text) } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Brand") } @@ -82,11 +82,7 @@ TabView width: scrollView.columnWidth; text: properties.supplier; readOnly: !base.editingEnabled; - onEditingFinished: - { - base.setMetaDataEntry("brand", properties.supplier, text); - pane.objectList.currentIndex = pane.getIndexById(base.containerId); - } + onEditingFinished: base.updateMaterialSupplier(properties.supplier, text) } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") } @@ -95,15 +91,10 @@ TabView width: scrollView.columnWidth; text: properties.material_type; readOnly: !base.editingEnabled; - onEditingFinished: - { - base.setMetaDataEntry("material", properties.material_type, text); - pane.objectList.currentIndex = pane.getIndexById(base.containerId) - } + onEditingFinished: base.updateMaterialType(properties.material_type, text) } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") } - Row { width: scrollView.columnWidth height: parent.rowHeight @@ -128,13 +119,6 @@ TabView } } - // make sure the color stays connected after changing the color - Binding { - target: colorSelector - property: "color" - value: properties.color_code - } - // pretty color name text field ReadOnlyTextField { id: colorLabel; @@ -453,14 +437,28 @@ TabView return 0; } - function setName(old_value, new_value) - { - if(old_value != new_value) - { - Cura.ContainerManager.setContainerName(base.containerId, new_value); - // update material name label. not so pretty, but it works - materialProperties.name = new_value; - pane.objectList.currentIndex = pane.getIndexById(base.containerId) + // update the display name of the material + function updateMaterialDisplayName (old_name, new_name) { + + // don't change when new name is the same + if (old_name == new_name) { + return } + + // update the values + Cura.ContainerManager.setContainerName(base.containerId, new_name) + materialProperties.name = new_name + } + + // update the type of the material + function updateMaterialType (old_type, new_type) { + base.setMetaDataEntry("material", old_type, new_type) + materialProperties.material_type = new_type + } + + // update the supplier of the material + function updateMaterialSupplier (old_supplier, new_supplier) { + base.setMetaDataEntry("brand", old_supplier, new_supplier) + materialProperties.supplier = new_supplier } } diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 6b041b895a..228f9c8ea2 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -153,15 +153,6 @@ UM.ManagementPage forceActiveFocus() Cura.ContainerManager.createMaterial() } - - Connections - { - target: base.objectList.model - onItemsChanged: - { - base.objectList.currentIndex = base.getIndexById(Cura.MachineManager.activeMaterialId); - } - } }, // Duplicate button From 8b3bd71b36ac764dd8750a06108f6ba92fabd47f Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 12 Jan 2018 09:57:32 +0100 Subject: [PATCH 195/200] Get extruders list from GlobalStack itself CURA-4784 --- plugins/CuraEngineBackend/CuraEngineBackend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e7db4a12dd..e1035af679 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -722,7 +722,7 @@ class CuraEngineBackend(QObject, Backend): if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) - extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) + extruders = list(self._global_container_stack.extruders.values()) for extruder in extruders: extruder.propertyChanged.disconnect(self._onSettingChanged) @@ -733,7 +733,7 @@ class CuraEngineBackend(QObject, Backend): if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. self._global_container_stack.containersChanged.connect(self._onChanged) - extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) + extruders = list(self._global_container_stack.extruders.values()) for extruder in extruders: extruder.propertyChanged.connect(self._onSettingChanged) extruder.containersChanged.connect(self._onChanged) From 225b03e98ee1d92467176a1fd1ef3605ce0999c7 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 12 Jan 2018 09:58:06 +0100 Subject: [PATCH 196/200] No need for the extra extrudersAdded signal CURA-4784 --- cura/Settings/ExtruderManager.py | 4 ---- plugins/CuraEngineBackend/CuraEngineBackend.py | 1 - 2 files changed, 5 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index b5f9a35914..351843ae14 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -49,9 +49,6 @@ class ExtruderManager(QObject): ## Notify when the user switches the currently active extruder. activeExtruderChanged = pyqtSignal() - ## The signal notifies subscribers if extruders are added - extrudersAdded = pyqtSignal() - ## Gets the unique identifier of the currently active extruder stack. # # The currently active extruder stack is the stack that is currently being @@ -409,7 +406,6 @@ class ExtruderManager(QObject): if extruders_changed: self.extrudersChanged.emit(global_stack_id) - self.extrudersAdded.emit() self.setActiveExtruderIndex(0) ## Get all extruder values for a certain setting. diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index e1035af679..3272fb019d 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -88,7 +88,6 @@ class CuraEngineBackend(QObject, Backend): # self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - Application.getInstance().getExtruderManager().extrudersAdded.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished) From 7d6e41f6c8d6987f0cbfe731b1635a958e326caa Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 12 Jan 2018 10:36:21 +0100 Subject: [PATCH 197/200] Disable activate configuration button for now until sync functionality has been decided --- .../UM3NetworkPrinting/UM3InfoComponents.qml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/plugins/UM3NetworkPrinting/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/UM3InfoComponents.qml index 939c6bcb39..18b481a6ed 100644 --- a/plugins/UM3NetworkPrinting/UM3InfoComponents.qml +++ b/plugins/UM3NetworkPrinting/UM3InfoComponents.qml @@ -115,24 +115,8 @@ Item { tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura") text: catalog.i18nc("@action:button", "Activate Configuration") - visible: printerConnected && !isClusterPrinter() + visible: false // printerConnected && !isClusterPrinter() onClicked: manager.loadConfigurationFromPrinter() - - function isClusterPrinter() { - 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; - } - var clusterSize = Cura.MachineManager.printerOutputDevices[0].clusterSize; - // This is not a cluster printer or the cluster it is just one printer - if(clusterSize == undefined || clusterSize == 1) - { - return false; - } - return true;*/ - } } } From b086e9cb5346b4a0f5668f85d5090251da4310e1 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 12 Jan 2018 10:52:14 +0100 Subject: [PATCH 198/200] Fixes for UM3 network printing for multi build plate --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 4283042bf2..e36b9abef7 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -80,8 +80,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): Application.getInstance().getController().setActiveStage("MonitorStage") self.writeStarted.emit(self) - self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", []) - if not self._gcode: + gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", []) + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_list = gcode_dict[active_build_plate_id] + + if not gcode_list: # Unable to find g-code. Nothing to send return From 06f35096ad34a2ebe095655b6756688c81687c80 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 12 Jan 2018 10:54:48 +0100 Subject: [PATCH 199/200] Fix setting gcode list on output device --- plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index e36b9abef7..45c152ee2f 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -88,6 +88,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Unable to find g-code. Nothing to send return + self._gcode = gcode_list + if len(self._printers) > 1: self._spawnPrinterSelectionDialog() else: From d1b5744f57632305c4e01fe492d5de7a9a197748 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 12 Jan 2018 10:58:36 +0100 Subject: [PATCH 200/200] Also fix gcode output for legacy UM3 networking, switch to monitor AFTER starting transferring G-code to catch potential errors before --- .../UM3NetworkPrinting/ClusterUM3OutputDevice.py | 5 +++-- .../UM3NetworkPrinting/LegacyUM3OutputDevice.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index 45c152ee2f..7bdf6090de 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -76,8 +76,6 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._cluster_size = int(properties.get(b"cluster_size", 0)) def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): - # Notify the UI that a switch to the print monitor should happen - Application.getInstance().getController().setActiveStage("MonitorStage") self.writeStarted.emit(self) gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", []) @@ -95,6 +93,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): else: self.sendPrintJob() + # Notify the UI that a switch to the print monitor should happen + Application.getInstance().getController().setActiveStage("MonitorStage") + def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml") diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 786b97d034..a63adadd54 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -175,15 +175,18 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # Not authenticated, so unable to send job. return - # Notify the UI that a switch to the print monitor should happen - Application.getInstance().getController().setActiveStage("MonitorStage") self.writeStarted.emit(self) - self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list", []) - if not self._gcode: + gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", []) + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_list = gcode_dict[active_build_plate_id] + + if not gcode_list: # Unable to find g-code. Nothing to send return + self._gcode = gcode_list + errors = self._checkForErrors() if errors: text = i18n_catalog.i18nc("@label", "Unable to start a new print job.") @@ -229,6 +232,9 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # No warnings or errors, so we're good to go. self._startPrint() + # Notify the UI that a switch to the print monitor should happen + Application.getInstance().getController().setActiveStage("MonitorStage") + def _startPrint(self): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: